{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "1623eb6a5cce817",
   "metadata": {},
   "source": [
    "# Sequence to Sequence (Seq2Seq) Model\n",
    "由于算力限制，该文件将提交至阿里云服务器运行，具体运行方法请参考阿里云文档。\n",
    "\n",
    "部分注释使用了fitten code协助生成"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d906c983343979a5",
   "metadata": {},
   "source": [
    "# import libraries"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "initial_id",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:22.371971Z",
     "start_time": "2025-03-11T05:30:17.774427Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:15.770432Z",
     "iopub.status.busy": "2025-03-11T05:56:15.770183Z",
     "iopub.status.idle": "2025-03-11T05:56:20.229908Z",
     "shell.execute_reply": "2025-03-11T05:56:20.229258Z",
     "shell.execute_reply.started": "2025-03-11T05:56:15.770399Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=10, micro=14, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.1\n",
      "torch 2.5.1+cu124\n",
      "cuda:0\n"
     ]
    }
   ],
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "\n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42   # 设置随机种子，使得每次运行结果一致\n",
    "torch.manual_seed(seed)\n",
    "torch.cuda.manual_seed_all(seed)\n",
    "np.random.seed(seed)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "81a0f5b6a3e63eb2",
   "metadata": {},
   "source": [
    "# Load Data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "2c808aa668299f13",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:22.441274Z",
     "start_time": "2025-03-11T05:30:22.372976Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:20.231675Z",
     "iopub.status.busy": "2025-03-11T05:56:20.231282Z",
     "iopub.status.idle": "2025-03-11T05:56:20.319311Z",
     "shell.execute_reply": "2025-03-11T05:56:20.318772Z",
     "shell.execute_reply.started": "2025-03-11T05:56:20.231650Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "May I borrow this book?\n",
      "¿Puedo tomar prestado este libro?\n"
     ]
    }
   ],
   "source": [
    "import unicodedata\n",
    "import re\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "#因为西班牙语有一些是特殊字符，所以我们需要unicode转ascii，\n",
    "# unicode占字节数比ascii多，所以我们需要把unicode转ascii\n",
    "# 这样值变小了，因为unicode太大\n",
    "def unicode_to_ascii(s):\n",
    "    #NFD是转换方法，把每一个字节拆开，Mn是重音，所以去除\n",
    "    return ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn')\n",
    "\n",
    "#找个样本测试一下\n",
    "# 加u代表对字符串进行unicode编码\n",
    "en_sentence = u\"May I borrow this book?\"\n",
    "sp_sentence = u\"¿Puedo tomar prestado este libro?\"\n",
    "\n",
    "print(unicode_to_ascii(en_sentence))\n",
    "print(unicode_to_ascii(sp_sentence))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "e18fc8c6662321db",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:22.447300Z",
     "start_time": "2025-03-11T05:30:22.442322Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:20.320374Z",
     "iopub.status.busy": "2025-03-11T05:56:20.320024Z",
     "iopub.status.idle": "2025-03-11T05:56:20.325891Z",
     "shell.execute_reply": "2025-03-11T05:56:20.325301Z",
     "shell.execute_reply.started": "2025-03-11T05:56:20.320350Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "may i borrow this book ?\n",
      "¿ puedo tomar prestado este libro ?\n",
      "b'\\xc2\\xbf puedo tomar prestado este libro ?'\n"
     ]
    }
   ],
   "source": [
    "def preprocess_sentence(w):\n",
    "    \"\"\"\n",
    "    Preprocess a sentence.\n",
    "    :param w:   The sentence to preprocess.\n",
    "    :return:    The preprocessed sentence.\n",
    "    \"\"\"\n",
    "    #变为小写，去掉多余的空格，变成小写，id少一些\n",
    "    w = unicode_to_ascii(w.lower().strip())\n",
    "\n",
    "    # 在单词与跟在其后的标点符号之间插入一个空格\n",
    "    # eg: \"he is a boy.\" => \"he is a boy . \"\n",
    "    # Reference:- https://stackoverflow.com/questions/3645931/python-padding-punctuation-with-white-spaces-keeping-punctuation\n",
    "    w = re.sub(r\"([?.!,¿])\", r\" \\1 \", w)\n",
    "    #因为可能有多余空格，替换为一个空格，所以处理一下\n",
    "    w = re.sub(r'[\" \"]+', \" \", w)\n",
    "\n",
    "    # 除了 (a-z, A-Z, \".\", \"?\", \"!\", \",\")，将所有字符替换为空格，你可以保留一些标点符号\n",
    "    w = re.sub(r\"[^a-zA-Z?.!,¿]+\", \" \", w)\n",
    "\n",
    "    w = w.rstrip().strip()\n",
    "\n",
    "    return w\n",
    "\n",
    "print(preprocess_sentence(en_sentence))\n",
    "print(preprocess_sentence(sp_sentence))\n",
    "print(preprocess_sentence(sp_sentence).encode('utf-8'))  #¿是占用两个字节的"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d944f78d106754a",
   "metadata": {},
   "source": [
    "# 自定义数据集\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "f5197dee26119022",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:22.693302Z",
     "start_time": "2025-03-11T05:30:22.448301Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:20.327168Z",
     "iopub.status.busy": "2025-03-11T05:56:20.326707Z",
     "iopub.status.idle": "2025-03-11T05:56:21.088860Z",
     "shell.execute_reply": "2025-03-11T05:56:21.088305Z",
     "shell.execute_reply.started": "2025-03-11T05:56:20.327142Z"
    }
   },
   "outputs": [],
   "source": [
    "from pathlib import Path    #引入pathlib模块\n",
    "from torch.utils.data import Dataset, DataLoader, dataset\n",
    "\n",
    "\n",
    "class LangPairDataset(Dataset):\n",
    "    \"\"\"\n",
    "    Custom dataset for language pair.\n",
    "    \"\"\"\n",
    "    fpath = Path(r\"./data_spa_en/spa.txt\") #数据文件路径\n",
    "    cache_path = Path(r\"./.cache/lang_pair.npy\") #缓存文件路径\n",
    "    split_index = np.random.choice(a=[\"train\", \"test\"], replace=True, p=[0.9, 0.1], size=118964) #按照9:1划分训练集和测试集\n",
    "    def __init__(self, mode=\"train\", cache=False):\n",
    "        if cache or not self.cache_path.exists():#如果没有缓存，或者缓存不存在，就处理一下数据\n",
    "            self.cache_path.parent.mkdir(parents=True, exist_ok=True) #创建缓存文件夹，如果存在就忽略\n",
    "            with open(self.fpath, \"r\", encoding=\"utf8\") as file:\n",
    "                lines = file.readlines()\n",
    "                lang_pair = [[preprocess_sentence(w) for w in l.split('\\t')]  for l in lines] #处理数据，变成list((trg, src))的形式\n",
    "                trg, src = zip(*lang_pair) #分离出目标语言和源语言\n",
    "                trg=np.array(trg) #转换为numpy数组\n",
    "                src=np.array(src) #转换为numpy数组\n",
    "                np.save(self.cache_path, {\"trg\": trg, \"src\": src})  #保存为npy文件,方便下次直接读取,不用再处理\n",
    "        else:\n",
    "            lang_pair = np.load(self.cache_path, allow_pickle=True).item() #读取npy文件，allow_pickle=True允许读取字典\n",
    "            trg = lang_pair[\"trg\"]  #取出目标语言\n",
    "            src = lang_pair[\"src\"]  #取出源语言\n",
    "\n",
    "        self.trg = trg[self.split_index == mode] #按照index拿到训练集的 标签语言 --英语\n",
    "        self.src = src[self.split_index == mode] #按照index拿到训练集的源语言 --西班牙\n",
    "\n",
    "    def __getitem__(self, index):    #返回数据\n",
    "        return self.src[index], self.trg[index]\n",
    "\n",
    "    def __len__(self):    #返回数据长度\n",
    "        return len(self.src)\n",
    "\n",
    "\n",
    "train_ds = LangPairDataset(\"train\")  #训练集\n",
    "test_ds = LangPairDataset(\"test\")   #测试集"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "69fcc640aced2fa7",
   "metadata": {},
   "source": [
    "# Tokenize"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "b71a27e277b37f17",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:23.237055Z",
     "start_time": "2025-03-11T05:30:22.694316Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:21.089718Z",
     "iopub.status.busy": "2025-03-11T05:56:21.089528Z",
     "iopub.status.idle": "2025-03-11T05:56:21.720028Z",
     "shell.execute_reply": "2025-03-11T05:56:21.719434Z",
     "shell.execute_reply.started": "2025-03-11T05:56:21.089697Z"
    }
   },
   "outputs": [],
   "source": [
    "from collections import Counter  #引入Counter模块\n",
    "\n",
    "def get_word_idx(ds, mode=\"src\", threshold=2):\n",
    "    \"\"\"\n",
    "    Get word indices.\n",
    "    :param ds:  The dataset.\n",
    "    :param mode:    The mode of the dataset, \"src\" or \"trg\".\n",
    "    :param threshold:    The threshold of word frequency.\n",
    "    :return:    The word2idx and idx2word.\n",
    "    \"\"\"\n",
    "    #载入词表，看下词表长度，词表就像英语字典\n",
    "    word2idx = {\n",
    "        \"[PAD]\": 0,     # 填充 token\n",
    "        \"[BOS]\": 1,     # begin of sentence\n",
    "        \"[UNK]\": 2,     # 未知 token\n",
    "        \"[EOS]\": 3,     # end of sentence\n",
    "    }\n",
    "    idx2word = {value: key for key, value in word2idx.items()}  # 反向词表\n",
    "    index = len(idx2word)  # 词表长度\n",
    "    threshold = 1  # 出现次数低于此的token舍弃\n",
    "    #如果数据集有很多个G，那是用for循环的，不能' '.join\n",
    "    word_list = \" \".join([pair[0 if mode==\"src\" else 1] for pair in ds]).split()  # 取出源语言或目标语言的单词列表\n",
    "    counter = Counter(word_list) #统计词频,counter类似字典，key是单词，value是出现次数\n",
    "\n",
    "    for token, count in counter.items():    #遍历词频\n",
    "        if count >= threshold:#出现次数大于阈值的token加入词表\n",
    "            word2idx[token] = index #加入词表\n",
    "            idx2word[index] = token #加入反向词表\n",
    "            index += 1  #词表长度加1\n",
    "\n",
    "    return word2idx, idx2word    #返回词表和反向词表\n",
    "\n",
    "src_word2idx, src_idx2word = get_word_idx(train_ds, \"src\") #源语言词表  西班牙语\n",
    "trg_word2idx, trg_idx2word = get_word_idx(train_ds, \"trg\") #目标语言词表 英语"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "f463bc53b7cb9eb2",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:23.251383Z",
     "start_time": "2025-03-11T05:30:23.237055Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:21.722268Z",
     "iopub.status.busy": "2025-03-11T05:56:21.722027Z",
     "iopub.status.idle": "2025-03-11T05:56:21.763879Z",
     "shell.execute_reply": "2025-03-11T05:56:21.763175Z",
     "shell.execute_reply.started": "2025-03-11T05:56:21.722243Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "raw text----------\n",
      "['hello', 'world']\n",
      "['tokenize', 'text', 'datas', 'with', 'batch']\n",
      "['this', 'is', 'a', 'test']\n",
      "mask----------\n",
      "tensor([0, 0, 0, 0, 1, 1, 1])\n",
      "tensor([0, 0, 0, 0, 0, 0, 0])\n",
      "tensor([0, 0, 0, 0, 0, 0, 1])\n",
      "indices----------\n",
      "tensor([   1,   17, 3224,    3,    0,    0,    0])\n",
      "tensor([   1,    2, 3870,    2,  538,    2,    3])\n",
      "tensor([   1,  121,  233,  107, 1262,    3,    0])\n"
     ]
    }
   ],
   "source": [
    "# 设置Tokenizer\n",
    "class Tokenizer:\n",
    "    \"\"\"\n",
    "    Tokenizer class.\n",
    "    \"\"\"\n",
    "    def __init__(self, word2idx, idx2word, max_length=500, pad_idx=0, bos_idx=1, eos_idx=3, unk_idx=2):\n",
    "        self.word2idx = word2idx    #词表\n",
    "        self.idx2word = idx2word    #反向词表\n",
    "        self.max_length = max_length    #最大长度\n",
    "        self.pad_idx = pad_idx    #padding的index\n",
    "        self.bos_idx = bos_idx    #bos的index\n",
    "        self.eos_idx = eos_idx    #eos的index\n",
    "        self.unk_idx = unk_idx    #unk的index\n",
    "\n",
    "    def encode(self, text_list, padding_first=False, add_bos=True, add_eos=True, return_mask=False):\n",
    "        \"\"\"\n",
    "        如果padding_first == True，则padding加载前面，否则加载后面\n",
    "        return_mask: 是否返回mask(掩码），mask用于指示哪些是padding的，哪些是真实的token\n",
    "        :param text_list:    The text to encode.\n",
    "        :param padding_first:    Whether to padding load first.\n",
    "        :param add_bos:     Whether to add bos.\n",
    "        :param add_eos:     Whether to add eos.\n",
    "        :param return_mask: Whether to return mask.\n",
    "        :return:    The encoded text.\n",
    "        \"\"\"\n",
    "        max_length = min(self.max_length, add_eos + add_bos + max([len(text) for text in text_list]))    #最大长度\n",
    "        indices_list = []    #indices_list是一个list，里面是每个样本的index\n",
    "        for text in text_list:  #遍历每个样本\n",
    "            indices = [self.word2idx.get(word, self.unk_idx) for word in text[:max_length - add_bos - add_eos]] #如果词表中没有这个词，就用unk_idx代替，indices是一个list,里面是每个词的index,也就是一个样本的index\n",
    "            if add_bos:  #如果需要添加bos，就在开头添加\n",
    "                indices = [self.bos_idx] + indices\n",
    "            if add_eos:  #如果需要添加eos，就在结尾添加\n",
    "                indices = indices + [self.eos_idx]\n",
    "            if padding_first:#padding加载前面，超参可以调\n",
    "                indices = [self.pad_idx] * (max_length - len(indices)) + indices\n",
    "            else:#padding加载后面\n",
    "                indices = indices + [self.pad_idx] * (max_length - len(indices))\n",
    "            indices_list.append(indices)\n",
    "        input_ids = torch.tensor(indices_list) #转换为tensor\n",
    "        masks = (input_ids == self.pad_idx).to(dtype=torch.int64) #mask是一个和input_ids一样大小的tensor，0代表token，1代表padding，mask用于去除padding的影响\n",
    "        return input_ids if not return_mask else (input_ids, masks)\n",
    "\n",
    "\n",
    "    def decode(self, indices_list, remove_bos=True, remove_eos=True, remove_pad=True, split=False):\n",
    "        \"\"\"\n",
    "        Decode indices to text.\n",
    "        :param indices_list:    The indices to decode.\n",
    "        :param remove_bos:     Whether to remove bos.\n",
    "        :param remove_eos:     Whether to remove eos.\n",
    "        :param remove_pad:     Whether to remove pad.\n",
    "        :param split:         Whether to split the text into words.\n",
    "        :return:    The decoded text.\n",
    "        \"\"\"\n",
    "        text_list = []\n",
    "        for indices in indices_list:\n",
    "            text = []\n",
    "            for index in indices:\n",
    "                word = self.idx2word.get(index, \"[UNK]\") #如果词表中没有这个词，就用unk_idx代替\n",
    "                if remove_bos and word == \"[BOS]\":\n",
    "                    continue\n",
    "                if remove_eos and word == \"[EOS]\":#如果到达eos，就结束\n",
    "                    break\n",
    "                if remove_pad and word == \"[PAD]\":#如果到达pad，就结束\n",
    "                    break\n",
    "                text.append(word) #单词添加到列表中\n",
    "            text_list.append(\" \".join(text) if not split else text) #把列表中的单词拼接，变为一个句子\n",
    "        return text_list\n",
    "\n",
    "#两个相对于1个toknizer的好处是embedding的参数量减少\n",
    "src_tokenizer = Tokenizer(word2idx=src_word2idx, idx2word=src_idx2word) #源语言tokenizer\n",
    "trg_tokenizer = Tokenizer(word2idx=trg_word2idx, idx2word=trg_idx2word) #目标语言tokenizer\n",
    "\n",
    "raw_text = [\"hello world\".split(), \"tokenize text datas with batch\".split(), \"this is a test\".split()]\n",
    "indices,mask = trg_tokenizer.encode(raw_text, padding_first=False, add_bos=True, add_eos=True,return_mask=True)\n",
    "\n",
    "# 测试一下\n",
    "print(\"raw text\"+'-'*10)\n",
    "for raw in raw_text:\n",
    "    print(raw)\n",
    "print(\"mask\"+'-'*10)\n",
    "for m in mask:\n",
    "    print(m)\n",
    "print(\"indices\"+'-'*10)\n",
    "for index in indices:\n",
    "    print(index)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "844989a0021babf0",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:23.256148Z",
     "start_time": "2025-03-11T05:30:23.252455Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:21.764795Z",
     "iopub.status.busy": "2025-03-11T05:56:21.764583Z",
     "iopub.status.idle": "2025-03-11T05:56:21.768853Z",
     "shell.execute_reply": "2025-03-11T05:56:21.768289Z",
     "shell.execute_reply.started": "2025-03-11T05:56:21.764773Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decode text----------\n",
      "[BOS] hello world [EOS] [PAD] [PAD] [PAD]\n",
      "[BOS] [UNK] text [UNK] with [UNK] [EOS]\n",
      "[BOS] this is a test [EOS] [PAD]\n"
     ]
    }
   ],
   "source": [
    "# 测试一下decode\n",
    "decode_text = trg_tokenizer.decode(indices.tolist(), remove_bos=False, remove_eos=False, remove_pad=False)\n",
    "print(\"decode text\"+'-'*10)\n",
    "for decode in decode_text:\n",
    "    print(decode)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3be657c4e33a8041",
   "metadata": {},
   "source": [
    "# DataLoader"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "4f7794dd9be14d91",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:23.263117Z",
     "start_time": "2025-03-11T05:30:23.257475Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:21.769781Z",
     "iopub.status.busy": "2025-03-11T05:56:21.769568Z",
     "iopub.status.idle": "2025-03-11T05:56:21.775038Z",
     "shell.execute_reply": "2025-03-11T05:56:21.774544Z",
     "shell.execute_reply.started": "2025-03-11T05:56:21.769759Z"
    }
   },
   "outputs": [],
   "source": [
    "def collate_fct(batch):\n",
    "    \"\"\"\n",
    "    Collate function. 该函数用于将batch内的数据处理成统一的格式，方便后续的训练\n",
    "    :param batch:    The batch to collate.\n",
    "    :return:    The collated batch.\n",
    "    \"\"\"\n",
    "    src_words = [pair[0].split() for pair in batch] #取batch内第0列进行分词，赋给src_words\n",
    "    trg_words = [pair[1].split() for pair in batch] #取batch内第1列进行分词，赋给trg_words\n",
    "\n",
    "    # [PAD] [BOS] src [EOS]\n",
    "    encoder_inputs, encoder_inputs_mask = src_tokenizer.encode(\n",
    "        src_words, padding_first=True, add_bos=True, add_eos=True, return_mask=True\n",
    "        )   \n",
    "\n",
    "    # [BOS] trg [PAD]\n",
    "    decoder_inputs = trg_tokenizer.encode(\n",
    "        trg_words, padding_first=False, add_bos=True, add_eos=False, return_mask=False,\n",
    "        )\n",
    "\n",
    "    # trg [EOS] [PAD]\n",
    "    decoder_labels, decoder_labels_mask = trg_tokenizer.encode(\n",
    "        trg_words, padding_first=False, add_bos=False, add_eos=True, return_mask=True\n",
    "        )\n",
    "\n",
    "    return {\n",
    "        \"encoder_inputs\": encoder_inputs.to(device=device),\n",
    "        \"encoder_inputs_mask\": encoder_inputs_mask.to(device=device),\n",
    "        \"decoder_inputs\": decoder_inputs.to(device=device),\n",
    "        \"decoder_labels\": decoder_labels.to(device=device),\n",
    "        \"decoder_labels_mask\": decoder_labels_mask.to(device=device), #mask用于去除padding的影响，计算loss时用\n",
    "    } #当返回的数据较多时，用dict返回比较合理\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "c6036b691ece3da8",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:23.389777Z",
     "start_time": "2025-03-11T05:30:23.264344Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:21.775847Z",
     "iopub.status.busy": "2025-03-11T05:56:21.775673Z",
     "iopub.status.idle": "2025-03-11T05:56:21.911573Z",
     "shell.execute_reply": "2025-03-11T05:56:21.910984Z",
     "shell.execute_reply.started": "2025-03-11T05:56:21.775827Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "encoder_inputs\n",
      "tensor([[   0,    0,    0,    0,    1,  350, 4002, 2826, 2827,    5,    3],\n",
      "        [   1,   12, 2266,  706,   80,  294,   88,   83,  297,   14,    3]],\n",
      "       device='cuda:0')\n",
      "encoder_inputs_mask\n",
      "tensor([[1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],\n",
      "        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], device='cuda:0')\n",
      "decoder_inputs\n",
      "tensor([[   1,  197,  756, 1413,    5,    0,    0,    0,    0,    0],\n",
      "        [   1,  332,   90, 1090,  443,  158,   31,  680, 3688,   10]],\n",
      "       device='cuda:0')\n",
      "decoder_labels\n",
      "tensor([[ 197,  756, 1413,    5,    3,    0,    0,    0,    0,    0],\n",
      "        [ 332,   90, 1090,  443,  158,   31,  680, 3688,   10,    3]],\n",
      "       device='cuda:0')\n",
      "decoder_labels_mask\n",
      "tensor([[0, 0, 0, 0, 0, 1, 1, 1, 1, 1],\n",
      "        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], device='cuda:0')\n"
     ]
    }
   ],
   "source": [
    "# 测试一下\n",
    "sample_dl = DataLoader(train_ds, batch_size=2, shuffle=True, collate_fn=collate_fct)\n",
    "\n",
    "#两次执行这个代码效果不一样，因为每次执行都会shuffle\n",
    "for batch in sample_dl:\n",
    "    for key, value in batch.items():\n",
    "        print(key)\n",
    "        print(value)\n",
    "    break"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3997e8ab8403bf9f",
   "metadata": {},
   "source": [
    "# Seq2Seq Model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "326c13c60f1de3ea",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:23.395516Z",
     "start_time": "2025-03-11T05:30:23.390281Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:21.912503Z",
     "iopub.status.busy": "2025-03-11T05:56:21.912238Z",
     "iopub.status.idle": "2025-03-11T05:56:21.917600Z",
     "shell.execute_reply": "2025-03-11T05:56:21.916894Z",
     "shell.execute_reply.started": "2025-03-11T05:56:21.912481Z"
    }
   },
   "outputs": [],
   "source": [
    "class Encoder(nn.Module):\n",
    "    \"\"\"\n",
    "    Encoder class.\n",
    "    \"\"\"\n",
    "    def __init__(\n",
    "        self,\n",
    "        vocab_size, #词表大小\n",
    "        embedding_dim=256,  #embedding维度\n",
    "        hidden_dim=1024,    #隐藏层维度\n",
    "        num_layers=1,        #层数\n",
    "        ):\n",
    "        super().__init__()\n",
    "        self.embedding = nn.Embedding(vocab_size, embedding_dim)    #embedding层\n",
    "        self.gru = nn.GRU(embedding_dim, hidden_dim, num_layers=num_layers, batch_first=True)    #gru层\n",
    "\n",
    "    def forward(self, encoder_inputs):  #encoder的前向传播\n",
    "        # encoder_inputs.shape = [batch size, sequence length]\n",
    "        # bs, seq_len = encoder_inputs.shape\n",
    "        embeds = self.embedding(encoder_inputs)  #embedding层\n",
    "        # embeds.shape = [batch size, sequence length, embedding_dim]->[batch size, sequence length, hidden_dim]\n",
    "        seq_output, hidden = self.gru(embeds)        #gru层\n",
    "        # seq_output.shape = [batch size, sequence length, hidden_dim]，hidden.shape [ num_layers, batch size, hidden_dim]\n",
    "        return seq_output, hidden    #返回输出和隐藏层"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "5d62c5192865aada",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:23.585327Z",
     "start_time": "2025-03-11T05:30:23.396028Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:21.918834Z",
     "iopub.status.busy": "2025-03-11T05:56:21.918401Z",
     "iopub.status.idle": "2025-03-11T05:56:22.211874Z",
     "shell.execute_reply": "2025-03-11T05:56:22.211355Z",
     "shell.execute_reply.started": "2025-03-11T05:56:21.918812Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([2, 50, 1024])\n",
      "torch.Size([4, 2, 1024])\n",
      "tensor([[-0.0636,  0.0092,  0.0103,  ..., -0.0153, -0.0004, -0.0341],\n",
      "        [-0.0175,  0.0283,  0.0105,  ...,  0.0142,  0.0282, -0.0413]],\n",
      "       grad_fn=<SliceBackward0>)\n",
      "tensor([[-0.0636,  0.0092,  0.0103,  ..., -0.0153, -0.0004, -0.0341],\n",
      "        [-0.0175,  0.0283,  0.0105,  ...,  0.0142,  0.0282, -0.0413]],\n",
      "       grad_fn=<SliceBackward0>)\n"
     ]
    }
   ],
   "source": [
    "#把上面的Encoder写一个例子，看看输出的shape\n",
    "encoder = Encoder(vocab_size=100, embedding_dim=256, hidden_dim=1024, num_layers=4)\n",
    "encoder_inputs = torch.randint(0, 100, (2, 50))\n",
    "encoder_outputs, hidden = encoder(encoder_inputs)\n",
    "print(encoder_outputs.shape)\n",
    "print(hidden.shape)\n",
    "print(encoder_outputs[:,-1,:])\n",
    "print(hidden[-1,:,:]) #取最后一层的hidden\n",
    "# 这样是为了确保数据的正确性，防止错误"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3c2e2c9a63e54258",
   "metadata": {},
   "source": [
    "# Attention Bahdanau used in the paper"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "db2b53f371afce72",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:23.592685Z",
     "start_time": "2025-03-11T05:30:23.586670Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:22.212765Z",
     "iopub.status.busy": "2025-03-11T05:56:22.212497Z",
     "iopub.status.idle": "2025-03-11T05:56:22.219116Z",
     "shell.execute_reply": "2025-03-11T05:56:22.218540Z",
     "shell.execute_reply.started": "2025-03-11T05:56:22.212743Z"
    }
   },
   "outputs": [],
   "source": [
    "# 注意力机制在torch中也表现为一个层，这里使用BahdanauAttention，它是一种比较常用的注意力机制，也就是加权求和的注意力机制\n",
    "class BahdanauAttention(nn.Module):\n",
    "    \"\"\"\n",
    "    BahdanauAttention class.\n",
    "    \"\"\"\n",
    "    def __init__(self, hidden_dim=1024):\n",
    "        super().__init__()\n",
    "        self.Wk = nn.Linear(hidden_dim, hidden_dim) #对keys做运算，encoder的输出EO\n",
    "        self.Wq = nn.Linear(hidden_dim, hidden_dim) #对query做运算，decoder的隐藏状态\n",
    "        self.V = nn.Linear(hidden_dim, 1)\n",
    "\n",
    "    def forward(self, query, keys, values, attn_mask=None):\n",
    "        \"\"\"\n",
    "        正向传播\n",
    "        :param query: hidden state，是decoder的隐藏状态，shape = [batch size, hidden_dim]\n",
    "        :param keys: EO  [batch size, sequence length, hidden_dim]\n",
    "        :param values: EO  [batch size, sequence length, hidden_dim]\n",
    "        :param attn_mask:[batch size, sequence length],这里是encoder_inputs_mask\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        # query.shape = [batch size, hidden_dim] -->通过unsqueeze(-2)增加维度 [batch size, 1, hidden_dim]\n",
    "        # keys.shape = [batch size, sequence length, hidden_dim]\n",
    "        # values.shape = [batch size, sequence length, hidden_dim]\n",
    "        scores = self.V(F.tanh(self.Wk(keys) + self.Wq(query.unsqueeze(-2)))) #unsqueeze(-2)增加维度\n",
    "        # score.shape = [batch size, sequence length, 1]\n",
    "        if attn_mask is not None: #这个mask是encoder_inputs_mask，用来mask掉padding的部分,让padding部分socres为0\n",
    "            # attn_mask is a matrix of 0/1 element,\n",
    "            # 1 means to mask logits while 0 means do nothing\n",
    "            # here we add -inf to the element while mask == 1\n",
    "            attn_mask = (attn_mask.unsqueeze(-1)) * -1e16 #在最后增加一个维度，[batch size, sequence length] --> [batch size, sequence length, 1]\n",
    "            scores += attn_mask\n",
    "        scores = F.softmax(scores, dim=-2) #对每一个词的score做softmax\n",
    "        # score.shape = [batch size, sequence length, 1]\n",
    "        context_vector = torch.mul(scores, values).sum(dim=-2) #对每一个词的score和对应的value做乘法，然后在seq_len维度上求和，得到context_vector\n",
    "        # context_vector.shape = [batch size, hidden_dim]\n",
    "        #socres用于最后的画图\n",
    "        return context_vector, scores   #返回context_vector和scores\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "6cc066eb856c87e8",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:23.616506Z",
     "start_time": "2025-03-11T05:30:23.592685Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:22.220085Z",
     "iopub.status.busy": "2025-03-11T05:56:22.219782Z",
     "iopub.status.idle": "2025-03-11T05:56:22.248008Z",
     "shell.execute_reply": "2025-03-11T05:56:22.247371Z",
     "shell.execute_reply.started": "2025-03-11T05:56:22.220064Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([2, 1024])\n",
      "torch.Size([2, 50, 1])\n"
     ]
    }
   ],
   "source": [
    "# 测试一下Attention\n",
    "attention = BahdanauAttention(hidden_dim=1024)\n",
    "query = torch.randn(2, 1024) #Decoder的隐藏状态\n",
    "keys = torch.randn(2, 50, 1024) #EO\n",
    "values = torch.randn(2, 50, 1024) #EO\n",
    "attn_mask = torch.randint(0, 2, (2, 50))\n",
    "context_vector, scores = attention(query, keys, values, attn_mask)\n",
    "print(context_vector.shape)\n",
    "print(scores.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "a15c5ebed39a8754",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:23.626259Z",
     "start_time": "2025-03-11T05:30:23.619011Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:22.249142Z",
     "iopub.status.busy": "2025-03-11T05:56:22.248700Z",
     "iopub.status.idle": "2025-03-11T05:56:22.255842Z",
     "shell.execute_reply": "2025-03-11T05:56:22.255214Z",
     "shell.execute_reply.started": "2025-03-11T05:56:22.249120Z"
    }
   },
   "outputs": [],
   "source": [
    "class Decoder(nn.Module):\n",
    "    \"\"\"\n",
    "    Decoder class.\n",
    "    \"\"\"\n",
    "    def __init__(\n",
    "        self,\n",
    "        vocab_size, #词表大小\n",
    "        embedding_dim=256,  #embedding维度\n",
    "        hidden_dim=1024,    #隐藏层维度\n",
    "        num_layers=1,        #层数\n",
    "        ):\n",
    "        super(Decoder, self).__init__()\n",
    "        self.embedding = nn.Embedding(vocab_size, embedding_dim)\n",
    "        self.gru = nn.GRU(embedding_dim + hidden_dim, hidden_dim, num_layers=num_layers, batch_first=True)\n",
    "        self.fc = nn.Linear(hidden_dim, vocab_size) #最后分类,词典大小是多少，就输出多少个分类\n",
    "        self.dropout = nn.Dropout(0.6)\n",
    "        self.attention = BahdanauAttention(hidden_dim) #注意力得到的context_vector\n",
    "\n",
    "    def forward(self, decoder_input, hidden, encoder_outputs, attn_mask=None):\n",
    "        #attn_mask是encoder_inputs_mask\n",
    "        # decoder_input.shape = [batch size, 1]\n",
    "        assert len(decoder_input.shape) == 2 and decoder_input.shape[-1] == 1, f\"decoder_input.shape = {decoder_input.shape} is not valid\"\n",
    "        # hidden.shape = [batch size, hidden_dim]，decoder_hidden,而第一次使用的是encoder的hidden\n",
    "        assert len(hidden.shape) == 2, f\"hidden.shape = {hidden.shape} is not valid\"\n",
    "        # encoder_outputs.shape = [batch size, sequence length, hidden_dim]\n",
    "        assert len(encoder_outputs.shape) == 3, f\"encoder_outputs.shape = {encoder_outputs.shape} is not valid\"\n",
    "        # context_vector.shape = [batch_size, hidden_dim]\n",
    "        context_vector, attention_score = self.attention(\n",
    "            query=hidden, keys=encoder_outputs, values=encoder_outputs, attn_mask=attn_mask)\n",
    "        # decoder_input.shape = [batch size, 1]\n",
    "        embeds = self.embedding(decoder_input)\n",
    "        # embeds.shape = [batch size, 1, embedding_dim]\n",
    "        # context_vector.shape = [batch size, hidden_dim] -->unsqueeze(-2)增加维度 [batch size, 1, hidden_dim]\n",
    "        embeds = torch.cat([context_vector.unsqueeze(-2), embeds], dim=-1)\n",
    "        # 新的embeds.shape = [batch size, 1, embedding_dim + hidden_dim]\n",
    "        seq_output, hidden = self.gru(embeds)\n",
    "        # seq_output.shape = [batch size, 1, hidden_dim]\n",
    "        logits = self.fc(self.dropout(seq_output))\n",
    "        # logits.shape = [batch size, 1, vocab size]，attention_score = [batch size, sequence length, 1]\n",
    "        return logits, hidden, attention_score\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "ed1e3088023f43bf",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:23.638109Z",
     "start_time": "2025-03-11T05:30:23.627259Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:22.256967Z",
     "iopub.status.busy": "2025-03-11T05:56:22.256657Z",
     "iopub.status.idle": "2025-03-11T05:56:22.267041Z",
     "shell.execute_reply": "2025-03-11T05:56:22.266447Z",
     "shell.execute_reply.started": "2025-03-11T05:56:22.256946Z"
    }
   },
   "outputs": [],
   "source": [
    "# 正式引入Seq2Seq模型\n",
    "class Sequence2Sequence(nn.Module):\n",
    "    \"\"\"\n",
    "    Sequence2Sequence class.\n",
    "    \"\"\"\n",
    "    def __init__(\n",
    "        self,\n",
    "        src_vocab_size, #输入词典大小\n",
    "        trg_vocab_size, #输出词典大小\n",
    "        encoder_embedding_dim=256,\n",
    "        encoder_hidden_dim=1024, #encoder_hidden_dim和decoder_hidden_dim必须相同，是因为BahdanauAttention设计的\n",
    "        encoder_num_layers=4,    #encoder层数\n",
    "        decoder_embedding_dim=256,  \n",
    "        decoder_hidden_dim=1024,    \n",
    "        decoder_num_layers=4,    #decoder层数\n",
    "        bos_idx=1,    #开始标记\n",
    "        eos_idx=3,    #结束标记\n",
    "        max_length=512,  #最大长度\n",
    "        ):\n",
    "        super(Sequence2Sequence, self).__init__()\n",
    "        self.bos_idx = bos_idx  #开始标记\n",
    "        self.eos_idx = eos_idx  #结束标记\n",
    "        self.max_length = max_length    #最大长度\n",
    "        self.encoder = Encoder(         # encoder层数\n",
    "            src_vocab_size, #输入词典大小\n",
    "            embedding_dim=encoder_embedding_dim,    #embedding维度\n",
    "            hidden_dim=encoder_hidden_dim,    #隐藏层维度\n",
    "            num_layers=encoder_num_layers,    #层数\n",
    "            )\n",
    "        self.decoder = Decoder(         # decoder层数\n",
    "            trg_vocab_size, #输出词典大小\n",
    "            embedding_dim=decoder_embedding_dim,    #embedding维度\n",
    "            hidden_dim=decoder_hidden_dim,    #隐藏层维度\n",
    "            num_layers=decoder_num_layers,    #层数\n",
    "            )\n",
    "\n",
    "    def forward(self, *, encoder_inputs, decoder_inputs, attn_mask=None):\n",
    "        # encoding\n",
    "        encoder_outputs, hidden = self.encoder(encoder_inputs)\n",
    "        # decoding with teacher forcing\n",
    "        bs, seq_len = decoder_inputs.shape\n",
    "        logits_list = []\n",
    "        scores_list = []\n",
    "        for i in range(seq_len):#串行训练\n",
    "            # 每次迭代生成一个时间步的预测，存储在 logits_list 中，并且记录注意力分数（如果有的话）在 scores_list 中，最后将预测的logits和注意力分数拼接并返回。\n",
    "            logits, hidden, score = self.decoder(\n",
    "                decoder_inputs[:, i:i+1],\n",
    "                hidden[-1], #取最后一层的hidden，第一个时间步时，hidden是encoder的hidden，第二个时间步时，hidden是decoder的hidden\n",
    "                encoder_outputs,\n",
    "                attn_mask=attn_mask\n",
    "                )\n",
    "            logits_list.append(logits) #记录预测的logits，用于计算损失\n",
    "            scores_list.append(score) #记录注意力分数,用于画图\n",
    "\n",
    "        return torch.cat(logits_list, dim=-2), torch.cat(scores_list, dim=-1)\n",
    "\n",
    "    @torch.no_grad() #不计算梯度\n",
    "    def infer(self, encoder_input, attn_mask=None):\n",
    "        #infer用于预测\n",
    "        # encoder_input.shape = [1, sequence length],这只支持batch_size=1\n",
    "        # encoding\n",
    "        encoder_outputs, hidden = self.encoder(encoder_input)\n",
    "\n",
    "        # decoding，[[1]]\n",
    "        decoder_input = torch.Tensor([self.bos_idx]).reshape(1, 1).to(dtype=torch.int64) #shape为[1,1]，内容为开始标记\n",
    "        decoder_pred = None\n",
    "        pred_list = [] #预测序列\n",
    "        score_list = []\n",
    "        # 从开始标记 bos_idx 开始，迭代地生成序列，直到生成结束标记 eos_idx 或达到最大长度 max_length。\n",
    "        for _ in range(self.max_length):\n",
    "            logits, hidden, score = self.decoder(\n",
    "                decoder_input,\n",
    "                hidden[-1],\n",
    "                encoder_outputs,\n",
    "                attn_mask=attn_mask\n",
    "                )\n",
    "            # using greedy search,logits shape = [1, 1, vocab size]\n",
    "            decoder_pred = logits.argmax(dim=-1)\n",
    "            decoder_input = decoder_pred\n",
    "            pred_list.append(decoder_pred.reshape(-1).item()) #decoder_pred从(1,1)变为（1）标量\n",
    "            score_list.append(score) #记录注意力分数,用于画图\n",
    "\n",
    "            # stop at eos token\n",
    "            if decoder_pred == self.eos_idx:\n",
    "                break\n",
    "\n",
    "        # return\n",
    "        return pred_list, torch.cat(score_list, dim=-1)\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "4980add9e7e205e6",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:34.308735Z",
     "start_time": "2025-03-11T05:30:33.268394Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:22.268059Z",
     "iopub.status.busy": "2025-03-11T05:56:22.267788Z",
     "iopub.status.idle": "2025-03-11T05:56:23.189390Z",
     "shell.execute_reply": "2025-03-11T05:56:23.188657Z",
     "shell.execute_reply.started": "2025-03-11T05:56:22.268036Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([2, 50, 12469])\n",
      "torch.Size([2, 50, 50])\n"
     ]
    }
   ],
   "source": [
    "# 测试一下Seq2Seq模型\n",
    "model = Sequence2Sequence(src_vocab_size=len(src_word2idx), trg_vocab_size=len(trg_word2idx))\n",
    "#做model的前向传播，看看输出的shape\n",
    "encoder_inputs = torch.randint(0, 100, (2, 50))\n",
    "decoder_inputs = torch.randint(0, 100, (2, 50))\n",
    "attn_mask = torch.randint(0, 2, (2, 50))\n",
    "logits, scores = model(encoder_inputs=encoder_inputs, decoder_inputs=decoder_inputs, attn_mask=attn_mask)\n",
    "print(logits.shape)\n",
    "print(scores.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "1d6d31fbdbc40de",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:38.231003Z",
     "start_time": "2025-03-11T05:30:38.227443Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:23.190624Z",
     "iopub.status.busy": "2025-03-11T05:56:23.190272Z",
     "iopub.status.idle": "2025-03-11T05:56:23.195243Z",
     "shell.execute_reply": "2025-03-11T05:56:23.194457Z",
     "shell.execute_reply.started": "2025-03-11T05:56:23.190599Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The model has 72,968,118 trainable parameters\n"
     ]
    }
   ],
   "source": [
    "#计算一下model的总参数量\n",
    "def count_parameters(model):\n",
    "    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "\n",
    "print(f\"The model has {count_parameters(model):,} trainable parameters\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "ae5b83292aa1cd98",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:30:41.053347Z",
     "start_time": "2025-03-11T05:30:40.780896Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:23.198561Z",
     "iopub.status.busy": "2025-03-11T05:56:23.198173Z",
     "iopub.status.idle": "2025-03-11T05:56:23.744464Z",
     "shell.execute_reply": "2025-03-11T05:56:23.743814Z",
     "shell.execute_reply.started": "2025-03-11T05:56:23.198529Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The model has 72,968,118 trainable parameters\n"
     ]
    }
   ],
   "source": [
    "#初始化一个4层的GRU的model\n",
    "model=Sequence2Sequence(src_vocab_size=len(src_word2idx), trg_vocab_size=len(trg_word2idx), encoder_num_layers=4, decoder_num_layers=4)\n",
    "print(f\"The model has {count_parameters(model):,} trainable parameters\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ae7ecc6cf3ef992",
   "metadata": {},
   "source": [
    "# 训练Seq2Seq模型\n",
    "很明显由于参数过多，后面的代码放到阿里云"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5332d82f0033d009",
   "metadata": {},
   "source": [
    "## 损失函数设计"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "5337362cb64e057",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:23:40.768143Z",
     "start_time": "2025-03-11T05:23:40.763491Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:23.745356Z",
     "iopub.status.busy": "2025-03-11T05:56:23.745104Z",
     "iopub.status.idle": "2025-03-11T05:56:23.750095Z",
     "shell.execute_reply": "2025-03-11T05:56:23.749436Z",
     "shell.execute_reply.started": "2025-03-11T05:56:23.745334Z"
    }
   },
   "outputs": [],
   "source": [
    "# 采用交叉熵损失函数，并设计mask机制，使得padding部分的损失不计入损失函数的计算。\n",
    "def cross_entropy_with_padding(logits, labels, padding_mask=None):\n",
    "    # logits.shape = [batch size, sequence length, num of classes]\n",
    "    # labels.shape = [batch size, sequence length]\n",
    "    # padding_mask.shape = [batch size, sequence length] decoder_labels_mask\n",
    "    bs, seq_len, nc = logits.shape\n",
    "    loss = F.cross_entropy(logits.reshape(bs * seq_len, nc), labels.reshape(-1), reduce=False) #reduce=False表示不对batch求平均\n",
    "    if padding_mask is None:#如果没有padding_mask，就直接求平均\n",
    "        loss = loss.mean()\n",
    "    else:\n",
    "        # 如果提供了 padding_mask，则将padding填充部分的损失去除后计算有效损失的均值。首先，通过将 padding_mask reshape 成一维张量，并取 1 减去得到填充掩码。这样填充部分的掩码值变为 1，非填充部分变为 0。将损失张量与填充掩码相乘，这样填充部分的损失就会变为 0。然后，计算非填充部分的损失和（sum）以及非填充部分的掩码数量（sum）作为有效损失的均值计算。(因为上面我们设计的mask的token是0，所以这里是1-padding_mask)\n",
    "        padding_mask = 1 - padding_mask.reshape(-1) #将padding_mask reshape成一维张量，mask部分为0，非mask部分为1\n",
    "        loss = torch.mul(loss, padding_mask).sum() / padding_mask.sum()\n",
    "\n",
    "    return loss\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dfa663b3a58d32e9",
   "metadata": {},
   "source": [
    "## callback设计"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "c2d1fc4c6ba6b59",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:23:52.815051Z",
     "start_time": "2025-03-11T05:23:40.768143Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:23.750898Z",
     "iopub.status.busy": "2025-03-11T05:56:23.750714Z",
     "iopub.status.idle": "2025-03-11T05:56:23.985250Z",
     "shell.execute_reply": "2025-03-11T05:56:23.984579Z",
     "shell.execute_reply.started": "2025-03-11T05:56:23.750878Z"
    }
   },
   "outputs": [],
   "source": [
    "from torch.utils.tensorboard import SummaryWriter\n",
    "\n",
    "\n",
    "class TensorBoardCallback:\n",
    "    def __init__(self, log_dir, flush_secs=10):\n",
    "        \"\"\"\n",
    "        Args:\n",
    "            log_dir (str): dir to write log.\n",
    "            flush_secs (int, optional): write to dsk each flush_secs seconds. Defaults to 10.\n",
    "        \"\"\"\n",
    "        self.writer = SummaryWriter(log_dir=log_dir, flush_secs=flush_secs)\n",
    "\n",
    "    def draw_model(self, model, input_shape):\n",
    "        self.writer.add_graph(model, input_to_model=torch.randn(input_shape))\n",
    "\n",
    "    def add_loss_scalars(self, step, loss, val_loss):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/loss\",\n",
    "            tag_scalar_dict={\"loss\": loss, \"val_loss\": val_loss},\n",
    "            global_step=step,\n",
    "            )\n",
    "\n",
    "    def add_acc_scalars(self, step, acc, val_acc):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/accuracy\",\n",
    "            tag_scalar_dict={\"accuracy\": acc, \"val_accuracy\": val_acc},\n",
    "            global_step=step,\n",
    "        )\n",
    "\n",
    "    def add_lr_scalars(self, step, learning_rate):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/learning_rate\",\n",
    "            tag_scalar_dict={\"learning_rate\": learning_rate},\n",
    "            global_step=step,\n",
    "\n",
    "        )\n",
    "\n",
    "    def __call__(self, step, **kwargs):\n",
    "        # add loss\n",
    "        loss = kwargs.pop(\"loss\", None)\n",
    "        val_loss = kwargs.pop(\"val_loss\", None)\n",
    "        if loss is not None and val_loss is not None:\n",
    "            self.add_loss_scalars(step, loss, val_loss)\n",
    "        # add acc\n",
    "        acc = kwargs.pop(\"acc\", None)\n",
    "        val_acc = kwargs.pop(\"val_acc\", None)\n",
    "        if acc is not None and val_acc is not None:\n",
    "            self.add_acc_scalars(step, acc, val_acc)\n",
    "        # add lr\n",
    "        learning_rate = kwargs.pop(\"lr\", None)\n",
    "        if learning_rate is not None:\n",
    "            self.add_lr_scalars(step, learning_rate)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "a984521bae87a2eb",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:23:52.821718Z",
     "start_time": "2025-03-11T05:23:52.815051Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:23.986862Z",
     "iopub.status.busy": "2025-03-11T05:56:23.986053Z",
     "iopub.status.idle": "2025-03-11T05:56:23.993581Z",
     "shell.execute_reply": "2025-03-11T05:56:23.992913Z",
     "shell.execute_reply.started": "2025-03-11T05:56:23.986818Z"
    }
   },
   "outputs": [],
   "source": [
    "# 保存最优模型\n",
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
    "        \"\"\"\n",
    "        Save checkpoints each save_epoch epoch.\n",
    "        We save checkpoint by epoch in this implementation.\n",
    "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
    "\n",
    "        Args:\n",
    "            save_dir (str): dir to save checkpoint\n",
    "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
    "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
    "        \"\"\"\n",
    "        self.save_dir = save_dir\n",
    "        self.save_step = save_step\n",
    "        self.save_best_only = save_best_only\n",
    "        self.best_metrics = - np.inf\n",
    "\n",
    "        # mkdir\n",
    "        if not os.path.exists(self.save_dir):\n",
    "            os.mkdir(self.save_dir)\n",
    "\n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return\n",
    "\n",
    "        if self.save_best_only:\n",
    "            assert metric is not None\n",
    "            if metric >= self.best_metrics:\n",
    "                # save checkpoints\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
    "                # update best metrics\n",
    "                self.best_metrics = metric\n",
    "        else:\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c8531dfc7161759",
   "metadata": {},
   "source": [
    "## 定义早停机制"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "34b159f84d62e92d",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:23:52.827241Z",
     "start_time": "2025-03-11T05:23:52.822222Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:23.994826Z",
     "iopub.status.busy": "2025-03-11T05:56:23.994301Z",
     "iopub.status.idle": "2025-03-11T05:56:24.000255Z",
     "shell.execute_reply": "2025-03-11T05:56:23.999712Z",
     "shell.execute_reply.started": "2025-03-11T05:56:23.994790Z"
    }
   },
   "outputs": [],
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.001):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute\n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = - np.inf\n",
    "        self.counter = 0\n",
    "\n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter\n",
    "            self.counter = 0\n",
    "        else:\n",
    "            self.counter += 1\n",
    "\n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "737d5cdc21d14e17",
   "metadata": {},
   "source": [
    "## 训练和评估"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "bedfd3aa952bdf5a",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:23:52.833739Z",
     "start_time": "2025-03-11T05:23:52.828241Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:24.001588Z",
     "iopub.status.busy": "2025-03-11T05:56:24.001034Z",
     "iopub.status.idle": "2025-03-11T05:56:24.006619Z",
     "shell.execute_reply": "2025-03-11T05:56:24.006045Z",
     "shell.execute_reply.started": "2025-03-11T05:56:24.001550Z"
    }
   },
   "outputs": [],
   "source": [
    "# 定义训练函数\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    for batch in dataloader:\n",
    "        encoder_inputs = batch[\"encoder_inputs\"]\n",
    "        encoder_inputs_mask = batch[\"encoder_inputs_mask\"]\n",
    "        decoder_inputs = batch[\"decoder_inputs\"]\n",
    "        decoder_labels = batch[\"decoder_labels\"]\n",
    "        decoder_labels_mask = batch[\"decoder_labels_mask\"]\n",
    "\n",
    "        # 前向计算\n",
    "        logits, _ = model(\n",
    "            encoder_inputs=encoder_inputs,\n",
    "            decoder_inputs=decoder_inputs,\n",
    "            attn_mask=encoder_inputs_mask\n",
    "            ) #model就是seq2seq模型\n",
    "        loss = loss_fct(logits, decoder_labels, padding_mask=decoder_labels_mask)         # 验证集损失\n",
    "        loss_list.append(loss.cpu().item())\n",
    "\n",
    "    return np.mean(loss_list)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "990543a960a5dbe5",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T02:50:43.763400Z",
     "start_time": "2025-03-11T02:50:41.274212Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:24.007904Z",
     "iopub.status.busy": "2025-03-11T05:56:24.007600Z",
     "iopub.status.idle": "2025-03-11T05:56:26.039719Z",
     "shell.execute_reply": "2025-03-11T05:56:26.039076Z",
     "shell.execute_reply.started": "2025-03-11T05:56:24.007871Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model,\n",
    "    train_loader,\n",
    "    val_loader,\n",
    "    epoch,\n",
    "    loss_fct,\n",
    "    optimizer,\n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "\n",
    "    global_step = 1\n",
    "    model.train() # 切换到训练模式\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for batch in train_loader:\n",
    "                encoder_inputs = batch[\"encoder_inputs\"]\n",
    "                encoder_inputs_mask = batch[\"encoder_inputs_mask\"]\n",
    "                decoder_inputs = batch[\"decoder_inputs\"]\n",
    "                decoder_labels = batch[\"decoder_labels\"]\n",
    "                decoder_labels_mask = batch[\"decoder_labels_mask\"]\n",
    "\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "\n",
    "                # 前向计算\n",
    "                logits, _ = model(\n",
    "                    encoder_inputs=encoder_inputs,\n",
    "                    decoder_inputs=decoder_inputs,\n",
    "                    attn_mask=encoder_inputs_mask\n",
    "                    )\n",
    "                loss = loss_fct(logits, decoder_labels, padding_mask=decoder_labels_mask)\n",
    "\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "\n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"step\": global_step\n",
    "                })\n",
    "\n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval() # 切换到验证模式\n",
    "                    val_loss = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"step\": global_step\n",
    "                    })\n",
    "                    model.train() # 切换到训练模式\n",
    "\n",
    "                    # 1. 使用 tensorboard 可视化\n",
    "                    if tensorboard_callback is not None:\n",
    "                        tensorboard_callback(\n",
    "                            global_step,\n",
    "                            loss=loss, val_loss=val_loss,\n",
    "                            lr=optimizer.param_groups[0][\"lr\"],\n",
    "                            )\n",
    "\n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), metric=-val_loss)\n",
    "\n",
    "                    # 3. 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(-val_loss)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "\n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "            pbar.set_postfix({\"epoch\": epoch_id, \"loss\": loss, \"val_loss\": val_loss}) # 更新进度条\n",
    "\n",
    "    return record_dict\n",
    "\n",
    "\n",
    "epoch = 20\n",
    "batch_size = 64\n",
    "\n",
    "model = Sequence2Sequence(src_vocab_size=len(src_word2idx), trg_vocab_size=len(trg_word2idx))\n",
    "train_dl = DataLoader(train_ds, batch_size=batch_size, shuffle=True, collate_fn=collate_fct)\n",
    "test_dl = DataLoader(test_ds, batch_size=batch_size, shuffle=False, collate_fn=collate_fct)\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失\n",
    "loss_fct = cross_entropy_with_padding\n",
    "# 2. 定义优化器 采用 adam\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=0.001)\n",
    "\n",
    "# 1. tensorboard 可视化\n",
    "if not os.path.exists(\"runs\"):\n",
    "    os.mkdir(\"runs\")\n",
    "exp_name = \"translate-seq2seq\"\n",
    "tensorboard_callback = TensorBoardCallback(f\"runs/{exp_name}\")\n",
    "# tensorboard_callback.draw_model(model, [1, MAX_LENGTH])\n",
    "# 2. save best\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(\n",
    "    f\"{exp_name}\", save_step=200, save_best_only=True)\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5)\n",
    "\n",
    "model = model.to(device)\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "5dfd350cbe63ccf3",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T02:52:03.473111Z",
     "start_time": "2025-03-11T02:50:43.764770Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T05:56:26.040806Z",
     "iopub.status.busy": "2025-03-11T05:56:26.040410Z",
     "iopub.status.idle": "2025-03-11T06:25:01.185118Z",
     "shell.execute_reply": "2025-03-11T06:25:01.184550Z",
     "shell.execute_reply.started": "2025-03-11T05:56:26.040781Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "  0%|          | 0/33520 [00:00<?, ?it/s]/usr/local/lib/python3.10/site-packages/torch/nn/_reduction.py:51: UserWarning: size_average and reduce args will be deprecated, please use reduction='none' instead.\n",
      "  warnings.warn(warning.format(ret))\n",
      " 47%|████▋     | 15799/33520 [28:35<32:03,  9.21it/s, epoch=8, loss=3.72, val_loss=3.69]  "
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 9 / global_step 15800\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "# 训练模型\n",
    "record = training(\n",
    "    model,\n",
    "    train_dl,\n",
    "    test_dl,\n",
    "    epoch,\n",
    "    loss_fct,\n",
    "    optimizer,\n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=200\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "ffde8e76df3cfdaa",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T02:52:03.474116Z",
     "start_time": "2025-03-11T02:52:03.474116Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T06:25:01.186168Z",
     "iopub.status.busy": "2025-03-11T06:25:01.185948Z",
     "iopub.status.idle": "2025-03-11T06:25:01.325407Z",
     "shell.execute_reply": "2025-03-11T06:25:01.324894Z",
     "shell.execute_reply.started": "2025-03-11T06:25:01.186146Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 画图\n",
    "import matplotlib.pyplot as plt\n",
    "plt.plot([i[\"step\"] for i in record[\"train\"]], [i[\"loss\"] for i in record[\"train\"]], label=\"train\")\n",
    "plt.plot([i[\"step\"] for i in record[\"val\"]], [i[\"loss\"] for i in record[\"val\"]], label=\"val\")\n",
    "plt.grid()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8d2b9a52913cf4a7",
   "metadata": {},
   "source": [
    "# 推理和预测"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "74d43d1991c5e34a",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:32:08.017786Z",
     "start_time": "2025-03-11T05:32:07.064969Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T06:25:01.326361Z",
     "iopub.status.busy": "2025-03-11T06:25:01.326092Z",
     "iopub.status.idle": "2025-03-11T06:25:02.053896Z",
     "shell.execute_reply": "2025-03-11T06:25:02.053235Z",
     "shell.execute_reply.started": "2025-03-11T06:25:01.326339Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# load checkpoints,如何上线\n",
    "model = Sequence2Sequence(src_vocab_size=len(src_word2idx), trg_vocab_size=len(trg_word2idx), encoder_num_layers=4, decoder_num_layers=4)\n",
    "model.load_state_dict(torch.load(f\"./translate-seq2seq/best.ckpt\", weights_only=True,map_location=\"cpu\"))\n",
    "\n",
    "class Translator:\n",
    "    def __init__(self, model, src_tokenizer, trg_tokenizer):\n",
    "        self.model = model\n",
    "        self.model.eval() # 切换到验证模式\n",
    "        self.src_tokenizer = src_tokenizer\n",
    "        self.trg_tokenizer = trg_tokenizer\n",
    "\n",
    "    def draw_attention_map(self, scores, src_words_list, trg_words_list):\n",
    "        \"\"\"绘制注意力热力图\n",
    "\n",
    "        Args:\n",
    "            - scores (numpy.ndarray): shape = [source sequence length, target sequence length]\n",
    "        \"\"\"\n",
    "        plt.matshow(scores.T, cmap='viridis') # 注意力矩阵,显示注意力分数值\n",
    "        # 获取当前的轴\n",
    "        ax = plt.gca()\n",
    "\n",
    "        # 设置热图中每个单元格的分数的文本\n",
    "        for i in range(scores.shape[0]): #输入\n",
    "            for j in range(scores.shape[1]): #输出\n",
    "                ax.text(j, i, f'{scores[i, j]:.2f}',  # 格式化数字显示\n",
    "                               ha='center', va='center', color='k')\n",
    "\n",
    "        plt.xticks(range(scores.shape[0]), src_words_list)\n",
    "        plt.yticks(range(scores.shape[1]), trg_words_list)\n",
    "        plt.show()\n",
    "\n",
    "    def __call__(self, sentence):\n",
    "        sentence = preprocess_sentence(sentence) # 预处理句子，标点符号处理等\n",
    "        encoder_input, attn_mask = self.src_tokenizer.encode(\n",
    "            [sentence.split()],\n",
    "            padding_first=True,\n",
    "            add_bos=True,\n",
    "            add_eos=True,\n",
    "            return_mask=True,\n",
    "            ) # 对输入进行编码，并返回encode_piadding_mask\n",
    "        encoder_input = torch.Tensor(encoder_input).to(dtype=torch.int64) # 转换成tensor\n",
    "\n",
    "        preds, scores = model.infer(encoder_input=encoder_input, attn_mask=attn_mask) #预测\n",
    "\n",
    "        trg_sentence = self.trg_tokenizer.decode([preds], split=True, remove_eos=False)[0] #通过tokenizer转换成文字\n",
    "\n",
    "        src_decoded = self.src_tokenizer.decode(\n",
    "            encoder_input.tolist(),\n",
    "            split=True,\n",
    "            remove_bos=False,\n",
    "            remove_eos=False\n",
    "            )[0] #对输入编码id进行解码，转换成文字,为了画图\n",
    "\n",
    "        self.draw_attention_map(\n",
    "            scores.squeeze(0).numpy(),\n",
    "            src_decoded, # 注意力图的源句子\n",
    "            trg_sentence # 注意力图的目标句子\n",
    "            )\n",
    "        return \" \".join(trg_sentence[:-1])\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "38d21a2d6ba215d3",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:24:37.792693Z",
     "start_time": "2025-03-11T05:24:37.559679Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T06:25:02.054836Z",
     "iopub.status.busy": "2025-03-11T06:25:02.054603Z",
     "iopub.status.idle": "2025-03-11T06:25:02.293603Z",
     "shell.execute_reply": "2025-03-11T06:25:02.293046Z",
     "shell.execute_reply.started": "2025-03-11T06:25:02.054811Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlEAAAJECAYAAAA2Z3H2AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAZfxJREFUeJzt3X1clHW+//HXBcgM94gII4aiiSmpUN4gpqutbKTVL7fdVt3dNOtY226upt2olVpZnmzd7N7qtNnZ1s3jWcsy10o83ZiKimaZrplpsCWgKKAoIMz394c6OYCoFyoz8X4+HvMgLz7XNe/B6zG+5zsXk2WMMYiIiIjIWQlo6gAiIiIi/kglSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbGh2ZaoQYMGYVkWlmXx2WefNUmG3bt3ezKkpaWd1b6DBg1iwoQJ5yWXP0lKSmLu3LkX7P6MMdx2223ExMQ0eO5YlsVbb711wXJdaDNmzDjrc9Zf+cJzhS9kEJG6mm2JAhg7dix79uyhW7duXoXGsiyCg4Pp1KkTM2fOpPb/GefLL7/kV7/6Fa1bt8bhcNC5c2emTZvG4cOHveY2b97M//t//4+4uDicTidJSUkMHz6coqIiABITE9mzZw+TJk26YI9ZGmf58uXMnz+fpUuXes6d+uzZs4chQ4Zc4HQXzt133012dnZTx7hgGnquOPm2du1azz5Hjhxh+vTpdO7cGYfDQWxsLDfeeCNffvml17EPHz7MlClTuPjii3E6nbRu3ZqBAweyZMkSz8zixYtZt27dBXu8InJmgpo6QFMKDQ3F5XJ5bVuxYgWXXnoplZWVrFq1iv/4j/+gTZs23HrrrQCsXbuWzMxMMjMzeffdd4mPj2fdunVMmjSJ7Oxs/u///o/g4GD27t3L4MGDufbaa3nvvfeIjo5m9+7dvP3225SXlwMQGBiIy+UiPDz8gj92sWfnzp20adOGfv361fv9qqoqgoOD65xXPzbh4eHN6rxt6LniZK1atQKgsrKSzMxM8vLymDNnDunp6RQWFjJr1izS09NZsWIFffv2BeB3v/sdOTk5PPPMM6SkpFBcXMzq1aspLi72HDcmJoaysrLz/ChF5KyZZmrgwIFm/Pjxnj/v2rXLAGbTpk1ec4MHDza///3vjTHGuN1uk5KSYnr16mVqamq85j777DNjWZb5z//8T2OMMW+++aYJCgoyR48ePW2W6dOnm9TU1LPOP27cOHPPPfeYli1bmvj4eDN9+nTP9+fMmWO6detmQkNDzUUXXWTuuOMOc/DgQa9jrFq1ygwcONCEhISY6Ohoc9VVV5n9+/cbY4ypqakxjz32mElKSjJOp9P06NHDLFq06LSZ7rzzTjN+/HgTHR1t4uLizEsvvWQOHTpkbr75ZhMeHm4uvvhis2zZMmOMMa+++qqJioryOsabb75pap+Wb7/9tunVq5dxOBymVatWZtiwYZ7vtW/f3jz66KNmzJgxJjw83CQmJpoXX3zRa//PP//cXHnllcbpdJqYmBgzduzYOj+LMzF69GgDeG7t27c3AwcONH/4wx/M+PHjTatWrcygQYOMMcYA5s033zznGc7EP//5T3PFFVeYqKgoExMTY6655hrz9ddfe76fk5Nj0tLSjMPhMD179jSLFy/2OvfP5O/Fzjnrr870ueJk//mf/2ksyzKfffaZ1/aamhrTq1cvk5KSYtxutzHGmKioKDN//vzT5jiT+xWRC6tZv513Ohs2bCA3N5f09HQAPvvsM7Zu3crEiRMJCPD+0aWmppKZmcnf//53AFwuF9XV1bz55pt13g48V1577TXCwsLIyclh9uzZPPzww3zwwQcABAQE8PTTT/Pll1/y2muvsXLlSu69917Pvp999hmDBw8mJSWFNWvWsGrVKq677jpqamoAmDVrFv/93//NvHnz+PLLL7nrrrv47W9/y0cffXTaTLGxsaxbt45x48Zxxx13cOONN9KvXz82btzIVVddxU033VTnrc9Teffdd/n5z3/O0KFD2bRpE9nZ2fTp08drZs6cOfTq1YtNmzbx+9//njvuuIPt27cDUF5eTlZWFi1btmT9+vUsWrSIFStWcOedd57xz/mEp556iocffpiLLrqIPXv2sH79es9jDg4O5tNPP2XevHl19juXGc5EeXk5EydOZMOGDWRnZxMQEMDPf/5z3G43hw4d4tprryUlJYXc3FxmzJjB3XfffV5yNGcLFizgZz/7GampqV7bAwICuOuuu9i6dSubN28Gjj1XLFu2jIMHDzZFVBFpjKZucU3lVK8uQ0JCTFhYmGnRooUBzG233eaZeeONNxp8JfjHP/7RhISEeP48depUExQUZGJiYszVV19tZs+ebQoKCursZ3clqn///l7bevfube6777565xctWmRatWrl+fPIkSPNFVdcUe9sRUWFCQ0NNatXr/bafuutt5qRI0eecabq6moTFhZmbrrpJs+2PXv2GMCsWbPmjFY8MjIyzG9+85tT3mf79u3Nb3/7W8+f3W63iYuLMy+88IIxxpiXXnrJtGzZ0hw6dMgz8+6775qAgIB6/y5O58knnzTt27f3esyXXXZZnTlOWok61xnO1t69ew1gvvjiC/Piiy+aVq1amSNHjni+/8ILL2glqgGne644+XaC0+n02udkGzduNIBZuHChMcaYjz76yFx00UWmRYsWplevXmbChAlm1apVdfbTSpSI79FKVC0LFy7ks88+Y/PmzfzP//wPS5YsYfLkyV4z5gxXlh599FEKCgqYN28el156KfPmzaNLly588cUX5yRrjx49vP7cpk0bz0XrK1asYPDgwbRt25aIiAhuuukmiouLPStAJ1ai6vP1119z+PBhfvazn3mufQkPD+e///u/2blz5xlnCgwMpFWrVnTv3t2zLT4+HsCT83QaylnffVqWhcvl8hx/27ZtpKamEhYW5pm54oorcLvdntWqxurZs2eD378QGU62Y8cORo4cSceOHYmMjCQpKQmAvLw8tm3bRo8ePXA6nZ75jIyMc56hOTjxXHHy7WRn+jzxk5/8hG+++Ybs7Gx++ctf8uWXXzJgwAAeeeSR85BaRM4llahaEhMT6dSpE127duXGG29kwoQJzJkzh4qKCjp37gwc+0exPtu2bfPMnNCqVStuvPFG/vSnP7Ft2zYSEhL405/+dE6ytmjRwuvPlmXhdrvZvXs31157LT169OAf//gHubm5PPfcc8CxC58BQkJCTnncQ4cOAcfeSjv5H4itW7fyv//7v2ed6eRtlmUB4Ha7CQgIqPMPzdGjR73+3FDOhu7T7Xafdr9z5eRy5Auuu+469u/fz8svv0xOTg45OTnAD3/3p3Mmfy/yw3PFybcTOnfu3ODzxImZE1q0aMGAAQO47777eP/993n44Yd55JFHzvjvTESahkrUaQQGBlJdXU1VVRVpaWl06dKFJ598ss4/0ps3b2bFihWMHDnylMcKDg7m4osv9vx23vmSm5uL2+1mzpw59O3bl86dO/P99997zfTo0eOUv6KekpKCw+EgLy+vzj8SiYmJ5yxn69atOXjwoNfPo/ar+YZynomuXbuyefNmr/v49NNPCQgI4JJLLrF9XF/NUFxczPbt23nggQcYPHgwXbt25cCBA15ZPv/8cyoqKjzbTv61fDizvxdp2IgRI1ixYoXnuqcT3G43Tz75JCkpKXWulzpZSkoK1dXVXn9PIuJ7VKJqKS4upqCggH//+9/885//5KmnnuLKK68kMjISy7J45ZVX2Lp1K7/4xS9Yt24deXl5LFq0iOuuu46MjAzPB2AuXbqU3/72tyxdupSvvvqK7du386c//Ylly5Zx/fXXn9fH0KlTJ44ePcozzzzDN998w1//+tc6FzxPmTKF9evX8/vf/57PP/+cf/3rX7zwwgvs27ePiIgI7r77bu666y5ee+01du7cycaNG3nmmWd47bXXzlnO9PR0QkNDmTp1Kjt37mTBggXMnz/fa2b69On8/e9/Z/r06Wzbto0vvviCxx9//Izv4ze/+Q1Op5PRo0ezZcsW/u///o9x48Zx0003ed5aPN8uZIaWLVvSqlUrXnrpJb7++mtWrlzJxIkTPd//9a9/jWVZjB07lq1bt7Js2bI6K6Nn8vfS1J599tnTvs17vp14rjj5dqL03HXXXfTp04frrruORYsWkZeXx/r16/nFL37Btm3beOWVVzyrsoMGDeLFF18kNzeX3bt3s2zZMqZOnep53hER36USVUtmZiZt2rQhKSmJ2267jaFDh7Jw4ULP9/v168fatWsJDAxkyJAhdOrUiSlTpjB69Gg++OADHA4HcOyVZGhoKJMmTSItLY2+ffvyP//zP/zXf/0XN91003l9DKmpqfz5z3/m8ccfp1u3bvztb39j1qxZXjOdO3fm/fffZ/PmzfTp04eMjAyWLFlCUNCxjw575JFHePDBB5k1axZdu3bl6quv5t1336VDhw7nLGdMTAyvv/46y5Yto3v37vz9739nxowZXjODBg1i0aJFvP3226SlpfHTn/70rD50MDQ0lPfee4/9+/fTu3dvfvnLXzJ48GCeffbZc/Y4fClDQEAAb7zxBrm5uXTr1o277rqLJ554wvP98PBw3nnnHb744gsuu+wy7r///jql9Ez+Xpravn37Tnt93vl24rni5NuJT6l3Op2sXLmSUaNGMXXqVDp16sTVV19NYGAga9eu9XxGFEBWVhavvfYaV111FV27dmXcuHFkZWXxP//zP030yETkTFnmTK9+/JEZNGgQaWlpF/R/GXIqM2bM4K233tJbJtIkdu/eTYcOHdi0aVOz+V+5nA1fea7Q35OI72nWK1HPP/884eHh5+y35c5WXl4e4eHhPPbYY01y/yJyZpr6uWLIkCF1Ph1dRJpes12J+u677zhy5AgA7dq1Izg4+IJnqK6uZvfu3QA4HI5zetG2yJnSCkfDfOG5whcyiEhdzbZEiYiIiDRGs347T0RERMQulSgRERERG1SiRERERGxQiRIRERGxQSWqkSorK5kxYwaVlZVNHeWM+Vtm5T2//C0v+F9mf8srImdGv53XSGVlZURFRVFaWuo3/4sGf8usvOeXv+UF/8vsb3lF5MxoJUpERETEBpUoERERERuCmjrAheJ2u/n++++JiIjw/N/Tz4WysjKvr/7A3zIr7/nlb3nB/zKfr7zGGA4ePEhCQgIBAXpNLHKhNZtrov7973/rf6siIj9K+fn5XHTRRU0dQ6TZaTYrUREREQB8uzGJyHD/eMX2887dmzqCiPiwao6yimWe5zcRubCaTYk68RZeZHgAkRH+UaKCrBZNHUFEfNnx9xHO5SUKInLm/KNNiIiIiPgYlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGvy5RgwYNYsKECef8uM+/WkLH3rsJTdpJxtB81m2qOOXs0aOGR/68n+S+x+YvG5zH8pXlXjMde+8msM3XdW53Ttl7TvLmm69ZZZax0ixmncmm1Ow/5azbuPnGbOVT809WmsWsNR+wzxR4zawyy1hh/rfO7V9mU7PM64+ZlVd5a3vuuedISkrC6XSSnp7OunXrTjl79OhRHn74YS6++GKcTiepqaksX77cayYpKQnLsurc/vCHPyivH+T1x8y+mDfI9qPxAYsXL6ZFixbn9JgLlxxk0ox9PP94HOmXOXnq5RKGjPyebavaERdb98f14OPF/O0fB3nxT3F06RTMex8e5he3FrDq7Yu4rLsDgJx/JlLjNp59tvyriqzh3/PL68IanbfA5PMVn9OVy4kkhnx2sIlP6GeyCLacdeZ3soUC8uhKT0KJYD+FfM5qepkribRaAtCHwRh+yHuIUjbxCXG0bXZ5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hw03jzj2W3SjxxXS1hXIY/fHAvDH/4jiyhu+48/zDjB0cBgLlxxkw+YK5j3R2pNz/NhoHp17gE4dgunQLohpj+8nIT6QYVeHnXW+2trRma2sJ9K0JIoY8thBDdW0IQmALWYdTkLoZB0roIl0IpeP+NZ8RSwuCsinjAN0pacnbzvTiV1sI9SEE0IYO/kSByG0JuFUMX60ef0xs/LqnKht4sSJjB49ml69etGnTx/mzp3rucYUYNSoUbRt25ZZs2YBMH78eAYOHMicOXO45ppreOONN9iwYQMvvfSSJ++ECROYOXMmycnJdOjQgQcffJCEhASGDRumvD6e1x8z+2peny1RTWX49RHsK65hxuz9FOytJu1SB8sWJBDf+tiPKv+7owSctH7Xr3cIrz/vYtrjxdw/q5jkDsEsfrUN3br8UODu+UM05Yfd/O6eIkrK3PTv42TZggSczsYvBLqsRI6aSr5hK5VUEEEUl9Efh3VslauCw1hYnvloK5ZuJp2dbOFrthBKOKn0I9yK8sy05xJqqGEbuVRzlGhiSaM/gVbj3n70x7z+mFl5dU7UNnz4cPbu3cu0adMoKCggLS2N5cuXe1b08/LyCDjpia1fv34sWLCABx54gKlTp5KcnMxbb71Ft27dPDP33nsv5eXl3HbbbZSUlNC/f3+WL1+O09n4t0yV9/zm9cfMvprXJz8natCgQaSlpTF37lzPtqSkJCZMmMCECRM82yzL4s033zyj1nimnxPlS87kc6JEpPk608+JEpHzwz/ahIiIiIiP8cm38z788MM623bv3l1nmw8uoomIiEgzoZUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSWqHs+/WkLH3rsJTdpJxtB81m2qaHB+0TuHSOn/LaFJO0m9Mo9l2eVe3zfGMH12MW1TdxHWYSdX/eo7dnxT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ERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbPDbEvXpp5/SvXt3WrRowbBhw5o6joiIiDQzQU0dwK6JEyeSlpbGP//5T8LDw5s6joiIiDQzfrsStXPnTn76059y0UUXER0d3dRxREREpJnx2RJVWVnJH//4R+Li4nA6nfTv35/169eze/duLMuiuLiYW265BcuymD9/flPHFRERkWbGZ0vUvffeyz/+8Q9ee+01Nm7cSKdOncjKyiIiIoI9e/YQGRnJ3Llz2bNnD8OHD2/quCIiItLM+GSJKi8v54UXXuCJJ55gyJAhpKSk8PLLLxMSEsJf/vIXXC4XlmURFRWFy+UiJCSkzjEqKyspKyvzuomIiIicKz5Zonbu3MnRo0e54oorPNtatGhBnz592LZt2xkdY9asWURFRXluiYmJ5yuuiIiINEM+WaLOhSlTplBaWuq55efnN3UkERER+RHxyRJ18cUXExwczKeffurZdvToUdavX09KSsoZHcPhcBAZGel1ExERETlXfPJzosLCwrjjjju45557iImJoV27dsyePZvDhw9z6623NnU8EREREd8sUQD/+Z//idvt5qabbuLgwYP06tWL9957j5YtWzZ1NBEREREsY4xp6hAXQllZGVFRURz4qiORET75LmYdWQlpTR1BRHxYtTnKhyyhtLRUlyyINAH/aBMiIiIiPkYlSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbGhyUvUhx9+iGVZlJSUNHUUERERkTN2wUvUoEGDmDBhwoW+WxEREZFzqslXokRERET80QUtUTfffDMfffQRTz31FJZlYVkWu3fvBiA3N5devXoRGhpKv3792L59u9e+S5Ys4fLLL8fpdNKxY0ceeughqqurL2R8EREREY8LWqKeeuopMjIyGDt2LHv27GHPnj0kJiYCcP/99zNnzhw2bNhAUFAQt9xyi2e/Tz75hFGjRjF+/Hi2bt3Kiy++yPz583n00UdPeV+VlZWUlZV53URERETOlQtaoqKioggODiY0NBSXy4XL5SIwMBCARx99lIEDB5KSksLkyZNZvXo1FRUVADz00ENMnjyZ0aNH07FjR372s5/xyCOP8OKLL57yvmbNmkVUVJTndqKsiYiIiJwLPnNNVI8ePTz/3aZNGwCKiooA2Lx5Mw8//DDh4eGe24nVrMOHD9d7vClTplBaWuq55efnn/8HISIiIs1GUFMHOKFFixae/7YsCwC32w3AoUOHeOihh7jhhhvq7Od0Ous9nsPhwOFwnIekIiIiIk1QooKDg6mpqTmrfS6//HK2b99Op06dzlMqERERkbNzwUtUUlISOTk57N69m/DwcM9qU0OmTZvGtddeS7t27fjlL39JQEAAmzdvZsuWLcycOfMCpBYRERHxdsGvibr77rsJDAwkJSWF1q1bk5eXd9p9srKyWLp0Ke+//z69e/emb9++PPnkk7Rv3/4CJBYRERGpyzLGmKYOcSGUlZURFRXFga86EhnhM9fTNygrIa2pI4iID6s2R/mQJZSWlhIZGdnUcUSaHf9oEyIiIiI+RiVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG2yVqP/93/+le/fuhISE0KpVKzIzMykvL2f9+vX87Gc/IzY2lqioKAYOHMjGjRu99rUsixdffJFrr72W0NBQunbtypo1a/j6668ZNGgQYWFh9OvXj507d3rtt2TJEi6//HKcTicdO3bkoYceorq62v4jFxEREWmEsy5Re/bsYeTIkdxyyy1s27aNDz/8kBtuuAFjDAcPHmT06NGsWrWKtWvXkpyczNChQzl48KDXMR555BFGjRrFZ599RpcuXfj1r3/N7bffzpQpU9iwYQPGGO68807P/CeffMKoUaMYP348W7du5cUXX2T+/Pk8+uijp8xZWVlJWVmZ101ERETkXLGMMeZsdti4cSM9e/Zk9+7dtG/fvsFZt9tNdHQ0CxYs4Nprrz12h5bFAw88wCOPPALA2rVrycjI4JVXXuGWW24B4I033mDMmDEcOXIEgMzMTAYPHsyUKVM8x3799de59957+f777+u97xkzZvDQQw/V2X7gq45ERvjHu5hZCWlNHUFEfFi1OcqHLKG0tJTIyMimjiPS7Jx1m0hNTWXw4MF0796dG2+8kZdffpkDBw4AUFhYyNixY0lOTiYqKorIyEgOHTpEXl6e1zF69Ojh+e/4+HgAunfv7rWtoqLCs3q0efNmHn74YcLDwz23sWPHsmfPHg4fPlxvzilTplBaWuq55efnn+1DFRERETmloLPdITAwkA8++IDVq1fz/vvv88wzz3D//feTk5PDHXfcQXFxMU899RTt27fH4XCQkZFBVVWV1zFatGjh+W/Lsk65ze12A3Do0CEeeughbrjhhjp5nE5nvTkdDgcOh+NsH56IiIjIGTnrEgXHSs4VV1zBFVdcwbRp02jfvj1vvvkmn376Kc8//zxDhw4FID8/n3379jU65OWXX8727dvp1KlTo48lIiIici6cdYnKyckhOzubq666iri4OHJycti7dy9du3YlOTmZv/71r/Tq1YuysjLuueceQkJCGh1y2rRpXHvttbRr145f/vKXBAQEsHnzZrZs2cLMmTMbfXwRERGRs3XW10RFRkby8ccfM3ToUDp37swDDzzAnDlzGDJkCK+88goHDhzg8ssv56abbuKPf/wjcXFxjQ6ZlZXF0qVLef/99+nduzd9+/blySefPO2F7SIiIiLny1n/dp6/KisrIyoqSr+dJyI/GvrtPJGm5R9tQkRERMTHqESJiIiI2KASJSIiImKDSpSIiIiIDSpRIiIiIjaoRImIiIjYoBIlIiIiYoNKlIiIiIgNKlEiIiIiNqhEiYiIiNigEiUiIiJig0qUiIiIiA0qUSIiIiI2qESJiIiI2KASJSIiImKDSpSIiIiIDSpRIiIiIjaoRImIiIjYoBIlIiIiYoNKlIiIiIgNKlEiIiIiNqhEiYiIiNigEiUiIiJig0qUiIiIiA0qUSIiIiI2qESJiIiI2KASJSIiImKDSpSIiIiIDSpRIiIiIjaoRImIiIjYoBIlIiIiYoNKlIiIiIgNKlEiIiIiNqhEiYiIiNigEiUiIiJig0qUiIiIiA0qUSIiIiI2BDV1gPOlsrKSyspKz5/LysqaMI2IiIj82PxoV6JmzZpFVFSU55aYmNjUkURERORH5EdboqZMmUJpaannlp+f39SRRERE5EfkR/t2nsPhwOFwNHUMERER+ZHy25WoZ599lsGDBzd1DBEREWmm/LZE7du3j507dzZ1DBEREWmm/LZEzZgxg927dzd1DBEREWmm/LZEiYiIiDQllSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEBpUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExAaVKBEREREbVKJEREREbFCJEhEREbFBJUpERETEhrMqUYMGDcKyLCzL4rPPPjtPkXw/g4iIiMhZr0SNHTuWPXv20K1bN3bv3u0pNLVva9eu9exz5MgRpk+fTufOnXE4HMTGxnLjjTfy5Zdfeh378OHDTJkyhYsvvhin00nr1q0ZOHAgS5Ys8cwsXryYdevWNeIhi4iIiDRe0NnuEBoaisvl8tq2YsUKLr30Uq9trVq1AqCyspLMzEzy8vKYM2cO6enpFBYWMmvWLNLT01mxYgV9+/YF4He/+x05OTk888wzpKSkUFxczOrVqykuLvYcNyYmhrKysrN+oCIiIiLn0lmXqPq0atWqTrE6Ye7cuaxZs4ZNmzaRmpoKQPv27fnHP/5Beno6t956K1u2bMGyLN5++22eeuophg4dCkBSUhI9e/Y8FxFFREREzqnzfmH5ggUL+NnPfuYpUJ47DgjgrrvuYuvWrWzevBkAl8vFsmXLOHjwYKPvt7KykrKyMq+biIiIyLlyTkpUv379CA8P97qd8NVXX9G1a9d69zux/auvvgLgpZdeYvXq1bRq1YrevXtz11138emnn9rKNGvWLKKiojy3xMREW8cRERERqc85KVELFy7ks88+87qdzBhzRsf5yU9+wjfffEN2dja//OUv+fLLLxkwYACPPPLIWWeaMmUKpaWlnlt+fv5ZH0NERETkVM7JNVGJiYl06tSp3u917tyZbdu21fu9E9s7d+7s2daiRQsGDBjAgAEDuO+++5g5cyYPP/ww9913H8HBwWecyeFw4HA4zuJRiIiIiJy5835N1IgRI1ixYoXnuqcT3G43Tz75JCkpKXWulzpZSkoK1dXVVFRUnO+oIiIiImfsnKxEFRcXU1BQ4LUtOjoap9PJXXfdxZIlS7juuuu8PuLgscceY9u2baxYsQLLsoBjH6Q5cuRIevXqRatWrdi6dStTp07lyiuvJDIy8lxEFRERETknzkmJyszMrLPt73//OyNGjMDpdLJy5Uoee+wxpk6dyrfffktERARXXnkla9eupVu3bp59srKyeO2115g6dSqHDx8mISGBa6+9lmnTpp2LmCIiIiLnTKNKVFJS0hldNB4aGsrMmTOZOXNmg3NTpkxhypQpjYkkIiIickGc9TVRzz//POHh4XzxxRfnI89pDRkypM6no4uIiIhcaGe1EvW3v/2NI0eOANCuXbvzEuh0/uu//qvJM4iIiIicVYlq27bt+crhVxlEREREzvtHHIiIiIj8GKlEiYiIiNigEiUiIiJig0qUiIiIiA0qUSIiIiI2qESJiIiI2KASJSIiImKDSpSIiIiIDSpRIiIiIjaoRImIiIjYoBIlIiIiYsNZ/b/z/JkxBoCyQ+4mTnLmqs3Rpo4gIj6smmPPESee30Tkwmo2JergwYMAtL98d9MGOSvfNHUAEfEDBw8eJCoqqqljiDQ7lmkmL2Hcbjfff/89ERERWJZ1zo5bVlZGYmIi+fn5REZGnrPjnk/+lll5zy9/ywv+l/l85TXGcPDgQRISEggI0NUZIhdas1mJCggI4KKLLjpvx4+MjPSLJ/OT+Vtm5T2//C0v+F/m85FXK1AiTUcvXURERERsUIkSERERsUElqpEcDgfTp0/H4XA0dZQz5m+Zlff88re84H+Z/S2viJyZZnNhuYiIiMi5pJUoERERERtUokRERERsUIkSERERsUElSkRERMQGlSgRERERG1SiRERERGxQiRIRERGxQSVKRERExIb/D9MkdwFEmz59AAAAAElFTkSuQmCC",
      "text/plain": [
       "<Figure size 400x685.714 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "'i m not to be a lot of the same .'"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 实例化翻译器\n",
    "translator = Translator(model.cpu(), src_tokenizer, trg_tokenizer)\n",
    "translator(u'hace mucho frio aqui .')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "72ad55361d4ad438",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-11T05:24:44.745298Z",
     "start_time": "2025-03-11T05:24:44.565990Z"
    },
    "execution": {
     "iopub.execute_input": "2025-03-11T06:25:02.294493Z",
     "iopub.status.busy": "2025-03-11T06:25:02.294242Z",
     "iopub.status.idle": "2025-03-11T06:25:02.548649Z",
     "shell.execute_reply": "2025-03-11T06:25:02.548016Z",
     "shell.execute_reply.started": "2025-03-11T06:25:02.294470Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAhIAAAIBCAYAAAAcf1aKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAa0lJREFUeJzt3X1c1HW+///HB5QZrgm5GDEUWjE1FcoLxNXVVoqy+snpbKnbWc3crnYzTcvUSq0svtlamNa6ta2223HzeNayzGMlni5MxESzTE+ZqbDmgKKAglzO5/cHMuvISPLJi4Ge99ttbuSb1+czT2bX4TnvudAwTdNERERExAK/ix1AREREWi8VCREREbFMRUJEREQsU5EQERERy1QkRERExDIVCREREbFMRUJEREQsU5EQERERy1QkRERExLKfXJEYNmwYhmFgGAaff/75Rcmwb98+d4aUlJSzOmbYsGFMnjz5vOaS86Mt/W936s+SkJBAdnb2Rc1zIfnCfYcvZBA53U+uSADceeedHDx4kF69enn8UjcMg4CAALp27crcuXM5/dPDv/rqK2699Vaio6Ox2Wx069aNWbNmUVlZ6TG3fft2/r//7/8jJiYGu91OQkICo0aNori4GID4+HgOHjzI1KlTL9jPLHKuffbZZ9x1110XO8YF1dx9x6mXTZs2uY85ceIEs2fPplu3bthsNqKiorjlllv46quvPM5dWVnJjBkz+NnPfobdbic6OpqhQ4eyatUq98zKlSvZvHnzBft5Rc5Gu4sd4GIICgrC4XB4rK1bt44rrriC6upqNmzYwG9/+1s6duzIhAkTANi0aRPp6emkp6fz7rvvEhsby+bNm5k6dSo5OTn87//+LwEBARw6dIjhw4dz44038t577xEREcG+fft4++23qaioAMDf3x+Hw0FISMgF/9lFzpXo6OiLHeGCa+6+41QdOnQAoLq6mvT0dAoKCpg/fz6pqakUFRWRlZVFamoq69atY+DAgQDcc8895OXlsXDhQnr27ElJSQkbN26kpKTEfd7IyEjKy8vP808p0jI/yR0Jbzp06IDD4aBLly7cdttt/PznP2fr1q0AmKbJhAkT6NGjBytXrmTAgAF06dKFW265hXfeeYfc3Fyef/55AD799FPKysr485//zJVXXkliYiJXX301zz//PImJiT8qo8vlYtq0aURGRuJwOJgzZ477ewUFBYwcOZKQkBDCwsK49dZbKSoqAhqeSvHz82PLli0e58vOzqZLly64XK4fletMWbOyskhMTCQwMJDk5GT++7//G4CjR49y2223ER0dTWBgIElJSSxZsuScZzidt634lJQU9+1oGAZ//vOf+bd/+zeCgoJISkri7bffPifXXVdXx3333Ud4eDhRUVE89thj7h0vwzB46623POYjIiJYunSp+8+FhYXceuutREREEBkZyciRI9m3b5/7+7fffjuZmZn84Q9/oGPHjnTo0IHf//731NbWWs5cUVHB2LFjCQkJoWPHjsyfP9/j+6ffns899xy9e/cmODiY+Ph4fve733H8+HHL199aNN53nHpp37490PB3LDc3l9WrV3PrrbfSpUsXBgwYwD/+8Q969OjBhAkT3P8/ePvtt5k5cyYjRowgISGBvn37MnHiRO64446L+eOJ/CAVCS+2bNlCfn4+qampAHz++efs3LmTKVOm4OfneZMlJyeTnp7O3//+dwAcDgd1dXW8+eabTZ4a+bFee+01goODycvLY968eTzxxBN88MEHuFwuRo4cyZEjR/joo4/44IMP+O677xg1ahTQcIefnp7e5Jf1kiVLuP3225v8TOdCVlYWf/3rX1m8eDFfffUVDzzwAP/xH//BRx99xGOPPcbOnTv5n//5H3bt2sUf//hHoqKiznkGKx5//HFuvfVWvvjiC0aMGMFtt93GkSNHfvR5X3vtNdq1a8fmzZtZsGABzz33HH/+85/P6tja2loyMjIIDQ3lk08+4dNPPyUkJITrrruOmpoa99z//u//smfPHv73f/+X1157jaVLl3qUkZZ66KGH+Oijj1i1ahXvv/8+H374obtce+Pn58cLL7zAV199xWuvvcb69euZNm2a5etvC5YtW8Y111xDcnKyx7qfnx8PPPAAO3fuZPv27UDDfceaNWs4duzYxYgqYp35EzN06FBz0qRJ7j/v3bvXBMzAwEAzODjYbN++vQmYd911l3vmjTfeMAFz27ZtXs95//33m4GBge4/z5w502zXrp0ZGRlpXnfddea8efNMp9PZ5LjZs2ebycnJZ5178ODBHmv9+/c3H374YfP99983/f39zYKCAvf3vvrqKxMwN2/ebJqmaS5fvty85JJLzKqqKtM0TTM/P980DMPcu3fvWV1/S1RVVZlBQUHmxo0bPdYnTJhgjhkzxrzpppvM8ePHn/Pr/SFdunQxn3/+eY+15ORkc/bs2aZpmiZgPvroo+7vHT9+3ATM//mf//lR1zt06FCzR48epsvlcq89/PDDZo8ePdzX++abb3ocEx4ebi5ZssQ0TdP829/+Zl5++eUex1dXV5uBgYHme++9Z5qmaY4bN87s0qWLWVdX55655ZZbzFGjRlnKfOzYMTMgIMD8r//6L/daSUmJGRgY6P774+32PNWKFSvMDh06WLp+X/RD9x2nXhrZ7XaPY061detWEzCXL19umqZpfvTRR+all15qtm/f3uzXr585efJkc8OGDU2Oa7zeM90fiVxo2pE4afny5Xz++eds376d//qv/2LVqlVMnz7dY8Y8yx2Gp556CqfTyeLFi7niiitYvHgx3bt358svv/xRGfv06ePx544dO1JcXMyuXbuIj48nPj7e/b2ePXsSERHBrl27AMjMzMTf358333wTgKVLl3L11VeTkJDwozJ58+2331JZWck111xDSEiI+/LXv/6VPXv2cO+99/LGG2+QkpLCtGnT2Lhx4znPYNWpt3FwcDBhYWHuF8n+GAMHDsQwDPef09LS2L17N/X19T947Pbt2/n2228JDQ1135aRkZFUVVWxZ88e99wVV1yBv7+/+8+N//+wYs+ePdTU1Lh35aDh+fnLL7/8jMesW7eO4cOH06lTJ0JDQ/nNb35DSUlJkxcjtzWN9x2nXk51tvcbv/jFL/juu+/IycnhV7/6FV999RVDhgzhySefPA+pRc4dFYmT4uPj6dq1Kz169OCWW25h8uTJzJ8/n6qqKrp16wbg/qV8ul27drlnGnXo0IFbbrmFP/zhD+zatYu4uDj+8Ic//KiMjc+7NjIM46xf3xAQEMDYsWNZsmQJNTU1LFu27Lw999r4vPi7777rcee6c+dO/vu//5vrr7+e/fv388ADD/D9998zfPhwHnzwwfOS5VR+fn5N7tRPfw3Bj7mNrTIMo9lcx48fp2/fvk1+WX3zzTf8+te/vqjZG+3bt48bb7yRPn368I9//IP8/HxefPFFAI+nX9qixvuOUy+NunXr1uz9RuNMo/bt2zNkyBAefvhh3n//fZ544gmefPLJNn8bSuumInEG/v7+1NXVUVNTQ0pKCt27d+f5559vcse8fft21q1bx5gxY854roCAAH72s5+537VxrvXo0YPCwkIKCwvdazt37qS0tJSePXu6137729+ybt06XnrpJerq6rj55pvPS56ePXtis9koKChocgfbuGsSHR3NuHHjeP3118nOzubll18+L1lOFR0dzcGDB91/Li8vZ+/evef9egHy8vI8/rxp0yaSkpLw9/dvkmv37t0ej+Kvuuoqdu/eTUxMTJPbMzw8/Lzk/dnPfkb79u09ch89epRvvvnG63x+fj4ul4v58+czcOBAunXrxvfff39esrUmo0ePZt26de7XQTRyuVw8//zz9OzZs8nrJ07Vs2dP6urqqKqqOt9RRSz7Sb7905uSkhKcTid1dXV8+eWXLFiwgKuvvpqwsDAAXn31Va655hr+/d//nRkzZuBwOMjLy2Pq1KmkpaW5P6Rn9erVvPHGG4wePZpu3bphmibvvPMOa9asOW/vTEhPT6d3797cdtttZGdnU1dXx+9+9zuGDh1Kv3793HM9evRg4MCBPPzww9xxxx0EBgaelzyhoaE8+OCDPPDAA7hcLgYPHkxZWRmffvopYWFh7Nmzh759+7rfbrt69Wp69OhxXrKc6pe//CVLly7lpptuIiIiglmzZnk8FXA+FRQUMGXKFO6++262bt3KwoUL3e+C+OUvf8miRYtIS0ujvr6ehx9+2GN34bbbbuPZZ59l5MiRPPHEE1x66aXs37+flStXMm3aNC699NJznjckJIQJEybw0EMP0aFDB2JiYnjkkUfO+MLcrl27Ultby8KFC7npppv49NNPWbx48TnPdbYWLVrEm2++SU5Oznm/rsb7jlNFRERgt9t54IEHWLVqFTfddJPH2z+ffvppdu3axbp169xPeQ0bNowxY8bQr18/OnTowM6dO5k5c6bH/ZCIL1KROCk9PR1o2Ino2LEjI0aM4KmnnnJ/f9CgQWzatInHH3+c66+/nmPHjtG5c2fGjRvHjBkzsNlsQMMjiKCgIKZOnUphYSE2m42kpCT+/Oc/85vf/Oa8ZDcMg1WrVjFx4kR+8Ytf4Ofnx3XXXcfChQubzE6YMIGNGzee97eUPfnkk0RHR5OVlcV3331HREQEV111FTNnzqSwsJAZM2awb98+AgMDGTJkCG+88cZ5zQMwY8YM9u7dy4033kh4eDhPPvnkBduRGDt2LCdOnGDAgAH4+/szadIk94c5zZ8/n/HjxzNkyBDi4uJYsGAB+fn57mODgoL4+OOPefjhh7n55ps5duwYnTp1Yvjw4ef1F8yzzz7L8ePHuemmmwgNDWXq1KmUlZV5nU1OTua5557jmWeeYcaMGfziF78gKyuLsWPHnrd8zTl8+LDH60fOp8b7jlP9/e9/Z/To0djtdtavX8/TTz/NzJkz2b9/P6GhoVx99dVs2rSJXr16uY/JyMjgtddeY+bMmVRWVhIXF8eNN97IrFmzLsjPIWKVYZ7tK4HaiGHDhpGSkuITH+07Z84c3nrrrQv6UbdPPvkkK1as4Isvvrhg1ynSFvjKfce+fftITExk27ZtZ/0R+yLn00/yNRIvvfQSISEhP/pdFFYVFBQQEhLC008/fcGu8/jx4+zYsYNFixYxceLEC3a9Im3Jxb7vuP7665t8iqbIxfaT25E4cOAAJ06cAKBz584EBARc8Ax1dXXuTyW02Wweb9s8X26//Xb+/ve/k5mZybJlyy7YawNE2gpfuO/whQwip/vJFQkRERE5d36ST22IiIjIuaEiISIiIpapSIiIiIhlKhIWVFdXM2fOHKqrqy92lCaUzRpfzearuUDZrPLlbCJW6MWWFpSXlxMeHk5ZWZnPfeKcslnjq9l8NRcom1W+nE3ECu1IiIiIiGUqEiIiImLZT+bf2nC5XHz//feEhoa6/5Ecq8rLyz2++hJls8ZXs/lqLlA2q85lNtM0OXbsGHFxcWf8B9VEzrefzGsk/vnPf16QT5AUEbnQCgsLz8u/AityNn4yOxKhoaEA7N+aQFiI7zX3f+vW+2JHEJFWpo5aNrDGff8mcjH8ZIpE49MZYSF+hIX6XpFoZ7S/2BFEpLU5uZ/8Y5+uFfkxfO83qoiIiLQaKhIiIiJimYqEiIiIWKYiISIiIpapSIiIiIhlKhIiIiJimYqEiIiIWKYiISIiIpapSIiIiIhlKhIiIiJimYqEiIiIWPaT+bc2fshLS0r5w0ulOA/Vk9wzgAVPRTPgSrvX2dpak/+38Ch//a9yDjjrufxn7cl6pAPX/TLYPXNZ/33s/2ddk2PvvT2cRVnRLcpWaH7Lfr6hhipCCOdyriTciPQ66zJd7OP/OMh+qjlBEKF0pTdRhsM9s8FcQxWVTY69lJ/R3biy1edStraXzVdz+Xq2F198kWeffRan00lycjILFy5kwIABXmdra2vJysritdde48CBA1x++eU888wzXHfdde6ZhIQE9u/f3+TY3/3ud7z44ottIpuv5vLlbK26SAwbNoyUlBSys7N/1HmWrzrG1DmHeemZGFKvtLPglVKuH/M9uzZ0Jiaq6U302DMl/Oc/jvGnP8TQvWsA731Yyb9PcLLh7Uu5srcNgLz/iafe9a9/oX3H/9WQMep7fnVTcJPzNcdpFvINX9CDqwgjkkJ2s41PGGRmEGA0LTp72IGTAnrQlyBCOUIRX7CRfubVhBmXADCA4Zj8K9txytjGJ8TQqdXnUra2l81Xc/l6tuXLlzNlyhQWL15Mamoq2dnZZGRk8PXXXxMTE9Nk/tFHH+X111/nlVdeoXv37rz33nv827/9Gxs3buTKKxsKzGeffUZ9fb37mB07dnDNNddwyy23tIlsvprL17MZpmmaPzzmm44cOUL79u3P6p/QLS8vJzw8nKPfXNbkX/9MG1FIvxQ7C59u2ClwuUy69N3HfXdE8PDES5qc69KUvcycdAm/Gx/hXvvVhIME2g3+9qKjyTzAA48d4t11lXy9sbPXf6kvIy7F63GbzRzCiHQ/EjFNkw28SzxdSTC6N5n/2FxNIt2JN7q617abufjjTy/De3P92vycwxxkENed9b8i6Ku5lK3tZfPVXL6Qrc6s5UNWUVZWRlhYmMf3UlNT6d+/P4sWLQLA5XIRHx/PxIkTmT59epPriYuL45FHHuH3v/+9e+3f//3fCQwM5PXXX/eabfLkyaxevZrdu3e36Hbz1Wy+msvXs7Xq10hERkaeVYloTk2NSf4X1QwfEuhe8/MzGD4kiNz8Kq/HVNeY2GyeN12g3eDTzd7na2pM/vMfxxg/OrRF/+O4TBfHKCWSf7VNwzCIJJZSSrweY+LCD3+PNX/8KeXwGa/DSQFxJJx1Nl/NpWxtL5uv5vL1bDU1NeTn55Oenu5e8/PzIz09ndzcXK/HVFdXY7d77qIEBgayYcOGM17H66+/zh133NEmsvlqLl/PBq28SAwbNozJkyf/qHMcPlJPfT3ERnv+5Y6N9qeouOlrHACuHRZE9p9K2f1dDS6XyQcfVfLmmgoOnmH+rbXHKS13MW5UmNfvn0kt1ZiYBOD5f4YAbNTgvbREEksBu6k0j2GaJiVmEcUcoPoM84c4QB21xJHQ6nMpW9vL5qu5fD3b4cOHqa+vJzY21mM9NjYWp9Pp9ZiMjAyee+45du/ejcvl4oMPPmDlypUcPHjQ6/xbb71FaWkpt99+e5vI5qu5fD0btPIi0Zzq6mrKy8s9LudK9hPRdE1sT88hBdg77+H+Rw5x++gw/Py8t7i/LCvnul8GEec4/y9JuZwUgghhI++xnpV8zbaGRzNnmD/APjrgwGYEnmGibedStraXzVdz+Xq2BQsWkJSURPfu3QkICOC+++5j/Pjx+Pl5/zXx6quvcv311xMXF/eTzearuS50tlb9YsvmZGVl8fjjj//gXFSkP/7+UHSo3mO96FA9sTHeb57oKH/eXNqRqioXJUddxDn8mfFUCZd1bt9kdn9hLTmfnOC/X/X+2onmtMeGgdHk0U0N1U0eBTUKMGwkM4h6s55aarBh51u+JJCQJrMnzAqOUEQfBrWJXMrW9rL5ai5fzxYVFYW/vz9FRUUe60VFRTgc3u+LoqOjeeutt6iqqqKkpIS4uDimT5/OZZdd1mR2//79rFu3jpUrV7aZbL6ay9ezQRvekZgxYwZlZWXuS2Fhode5gACDvn1srN9wwr3mcpms31BJWl/vdwaN7HY/OnVsR10drHy3gv8vo+k7MpYuLycmyp8b0lv2bg0AP8OPUCI4QrF7zTRNjlBMBB2aPdbf8MduBGJiUswBounYZOZ79hGAnShaVnJ8NZeytb1svprL17MFBATQt29fcnJy3Gsul4ucnBzS0tKaPdZut9OpUyfq6ur4xz/+wciRI5vMLFmyhJiYGG644YY2k81Xc/l6NmjDOxI2mw2bzXZWs5PvjmD8pGL6JtsYkNLw9s+KSpPbRze8kHPcxCI6Ofx5+pEoAPK2VnHgYB0pvWwcOFjHE/OP4HKZPPT7CI/zulwmS984xthbQ2nXrmUvXmnUmW7s5DPCzEsIJ5ICdlNPHR1PPme6w9yMnUC6Gr0BKDNLqD75fvZqTvAdOwHowuUe5zVNk4PspyNd8DNa3id9NZeytb1svprL17NNmTKFcePG0a9fPwYMGEB2djYVFRWMHz8egLFjx9KpUyeysrIAyMvL48CBA6SkpHDgwAHmzJmDy+Vi2rRpHud1uVwsWbKEcePG0a6dtV8hvprNV3P5erY2WyRaYtTIUA6X1DNn3hGch+pIucLGmmVxxEY33DyFB2o59WmlqiqTWc+U8F1BHSFBBtcPD+a1hbFEhHu+YHPdxycoOFDH+NEte5HlqRxGPLVmNd+xk2qqCCWcKxmM7eR71KuoxDjlGVYXLvawgxNU4E87OuCgFwNobwR4nPcIRVRR2eIXcfl6LmVre9l8NZevZxs1ahSHDh1i1qxZOJ1OUlJSWLt2rfsFewUFBR7Pl1dVVfHoo4/y3XffERISwogRI/jb3/5GRESEx3nXrVtHQUEBd9xxR5vL5qu5fD1bq/4ciZZ8IFVznyPhC870ORIiImfS3OdIiFwovvcbVURERFqNVv3UxocffnixI4iIiPykaUdCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJE56aUkpl/XfR1DCHtJGFLJ5W1Wz8yveOU7PwfsJSthD8tUFrMmp8Pi+aZrMnldCp+S9BCfu4dpbD7D7uxpL2QrNb9lgrmG9uZLNZg5l5pFm54vMf7LRfI/15kpyzfc5bB5skm2P+RUfm6tZb65kq/kxleaxNpXNV3MpW9vL5qu5AF588UUSEhKw2+2kpqayefPmZudXrFhB9+7dsdvt9O7dmzVr1jTJNmvWLDp27EhgYCDp6ens3r1b2S5QNl/NpSIBLF91jKlzDvPY1Ei2vBdPn542rh/zPcWH67zOb/zsBLfd6+SOX4eR/348I68L5ubxB9nxf9XumWdfLGXhq2W89Ew0ue9eSlCQH9eP+Z6qKleLsjnNQr7hCy6jJwNIJ5QItvEJNab3olNqHmYHecSRQCrpxBDHdjZy3Cxzz+znawr5lu5cRX9+iR/+bGMD9WZ9m8jmq7mUre1l89VcAMuXL2fKlCnMnj2brVu3kpycTEZGBsXFxV7nN27cyJgxY5gwYQLbtm0jMzOTzMxMduzY4Z6ZN28eL7zwAosXLyYvL4/g4GAyMjKoqmr+gZey/fhsvpoLwDBN02zREa1UeXk54eHhHP3mMsJCPftT2ohC+qXYWfh0NAAul0mXvvu4744IHp54SZNzjb7bSUWli3f+FudeG3RDIclX2PjjvBhM0+TSlH1MuSeCqfc2HF9WXk/H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cXFxZGZmKtt5zuarucBHi0RAQAD19f96WqBHjx784x//wDRN967Ep59+SmhoKJdeeumPvr5RI0M5XFLPnHlHcB6qI+UKG2uWxREb3XDzFB6oxe+UvZtB/QN5/SUHs54p4ZGsEpISA1i5pCO9uv+ruDz0+wgqKl3c81AxpeUuBg+ws2ZZHHZ7yzaBHEY8tWY137GTaqoIJZwrGYzNaNjZqKISg3/t1EQYUfQyU9nDDr5lB0GEkMwgQoxw90wXLqeeenaRTx21RBBFCoPxN87+aRdfzuaruZSt7WXz1VzQ8Pa/Q4cOMWvWLJxOJykpKaxdu9b94rqCggL8TrljGzRoEMuWLePRRx9l5syZJCUl8dZbb9GrVy/3zLRp06ioqOCuu+6itLSUwYMHs3btWuz2s99pVTZr2Xw1F/jo50jcddddfP755/zXf/0XISEhVFdX061bN8aPH899993H119/zW9/+1t+//vfM2fOnLM6Z3OfI+ELzvQ5EiIiZ9Lc50iIXCi+9xsVePDBB/H396dnz55ER0dTW1vLmjVr2Lx5M8nJydxzzz1MmDCBRx999GJHFRER+Unzyac2unXr1uQDMRISEn7wc8VFRETkwvLJHQkRERFpHVQkRERExDIVCREREbFMRUJEREQsU5EQERERy1QkRERExDIVCREREbFMRUJEREQsU5EQERERy1QkRERExDIVCREREbFMRUJEREQsU5EQERERy1QkRERExDIVCREREbFMRUJEREQsU5EQERERy1QkRERExDIVCREREbFMRUJEREQsU5EQERERy1QkRERExDIVCREREbFMRUJEREQsU5EQERERy1QkRERExDIViZNeWlLKZf33EZSwh7QRhWzeVtXs/Ip3jtNz8H6CEvaQfHUBa3IqPL5vmiaz55XQKXkvwYl7uPbWA+z+rsZStkLzWzaYa1hvrmSzmUOZeaTZ+SLzn2w032O9uZJc830OmwebZNtjfsXH5mrWmyvZan5MpXmsTWXz1VzK1vay+WougBdffJGEhATsdjupqals3ry52fkVK1bQvXt37HY7vXv3Zs2aNU2yzZo1i44dOxIYGEh6ejq7d+9WtguUzVdz+USRGDZsGJMnT75o17981TGmzjnMY1Mj2fJePH162rh+zPcUH67zOr/xsxPcdq+TO34dRv778Yy8Lpibxx9kx/9Vu2ee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YGxGw85GFZUY/GsnJ8KIopeZyh528C07CCKEZAYRYoS7Z7pwOfXUs4t86qglgihSGIy/cfZPu/hyNl/NpWxtL5uv5oKGt/8dOnSIWbNm4XQ6SUlJYe3ate4X1xUUFOB3yh3boEGDWLZsGY8++igzZ84kKSmJt956i169erlnpk2bRkVFBXfddRelpaUMHjyYtWvXYref/U6rslnL5qu5wIc+RyI/P58777yTu+++m61bt3LnnXcyf/587r77bv7jP/6DTz/9lPnz53PllVdy6NAhcnJy6NOnDzfccMNZXUdznyPhC870ORIiImfS3OdIiFwoPrMjMXbsWE6cOMGAAQPw9/dn0qRJ3HXXXQAsWbKEuXPnMnXqVA4cOEBUVBQDBw7kxhtvvMipRUREftp8YkfiQtCOhIi0NdqREF/ge79RRUREpNVQkRARERHLVCRERETEMhUJERERsUxFQkRERCxTkRARERHLVCRERETEMhUJERERsUxFQkRERCxTkRARERHLVCRERETEMhUJERERsUxFQkRERCxTkRARERHLVCRERETEMhUJERERsUxFQkRERCxTkRARERHLVCRERETEMhUJERERsUxFQkRERCxTkRARERHLVCRERETEMhUJERERsUxFQkRERCxTkRARERHLVCROemlJKZf130dQwh7SRhSyeVtVs/Mr3jlOz8H7CUrYQ/LVBazJqfD4vmmazJ5XQqfkvQQn7uHaWw+w+7saS9kKzW/ZYK5hvbmSzWYOZeaRZueLzH+y0XyP9eZKcs33OWwebJJtj/kVH5urWW+uZKv5MZXmsTaVzVdzKVvby+aruQBefPFFEhISsNvtpKamsnnz5mbnV6xYQffu3bHb7fTu3Zs1a9Y0yTZr1iw6duxIYGAg6enp7N69W9kuUDZfzaUiASxfdYypcw7z2NRItrwXT5+eNq4f8z3Fh+u8zm/87AS33evkjl+Hkf9+PCOvC+bm8QfZ8X/V7plnXyxl4atlvPRMNLnvXkpQkB/Xj/meqipXi7I5zUK+4QsuoycDSCeUCLbxCTWm96JTah5mB3nEkUAq6cQQx3Y2ctwsc8/s52sK+ZbuXEV/fokf/mxjA/VmfZvI5qu5lK3tZfPVXADLly9nypQpzJ49m61bt5KcnExGRgbFxcVe5zdu3MiYMWOYMGEC27ZtIzMzk8zMTHbs2OGe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AKZMmcK4cePo168fAwYMIDs7m4qKCsaPHw/A2LFj6dSpE1lZWQBMmjSJoUOHMn/+fG644QbeeOMNtmzZwssvv+zONnnyZObOnUtSUhKJiYk89thjxMXFtfgt+MrW8my+mgt8tEj87Gc/IyAggE8//ZQuXboAUFtby2effcbkyZPP+fWNGhnK4ZJ65sw7gvNQHSlX2FizLI7Y6Iabp/BALX6n7N0M6h/I6y85mPVMCY9klZCUGMDKJR3p1f1fxeWh30dQUeninoeKKS13MXiAnTXL4rDbW7YJ5DDiqTWr+Y6dVFNFKOFcyWBsRsPORhWVGBju+Qgjil5mKnvYwbfsIIgQkhlEiBHununC5dRTzy7yqaOWCKJIYTD+xtk/7eLL2Xw1l7K1vWy+mgsa3v536NAhZs2ahdPpJCUlhbVr17pfXFdQUIDfKXdsgwYNYtmyZTz66KPMnDmTpKQk3nrrLXr16uWemTZtGhUVFdx1112UlpYyePBg1q5di91+9jutymYtm6/mAh/+HInJkyezYsUKXn31VTp37sy8efN4++232bNnD5dccgkRERFkZ2dz++23n9X5mvscCV9wps+REBE5k+Y+R0LkQvHJHQmA//f//h8ul4vf/OY3HDt2jH79+vHee+9xySVNPyBKRERELg6f3ZE417QjISJtjXYkxBf43m9UERERaTVUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGx7KIXiQ8//BDDMCgtLb3YUURERKSFLniRGDZsGJMnT77QVysiIiLnwUXfkRAREZHW64IWidtvv52PPvqIBQsWYBgGhmGwb98+APLz8+nXrx9BQUEMGjSIr7/+2uPYVatWcdVVV2G327nssst4/PHHqauru5DxRURE5DQXtEgsWLCAtLQ07rzzTg4ePMjBgweJj48H4JFHHmH+/Pls2bKFdu3acccdd7iP++STTxg7diyTJk1i586d/OlPf2Lp0qU89dRTZ7yu6upqysvLPS4iIiJybl3QIhEeHk5AQABBQUE4HA4cDgf+/v4APPXUUwwdOpSePXsyffp0Nm7cSFVVFQCPP/4406dPZ9y4cVx22WVcc801PPnkk/zpT38643VlZWURHh7uvjQWFhERETl3fOY1En369HH/d8eOHQEoLi4GYPv27TzxxBOEhIS4L427GpWVlV7PN2PGDMrKytyXwsLC8/9DiIiI/MS0u9gBGrVv397934ZhAOByuQA4fvw4jz/+ODfffHOT4+x2u9fz2Ww2bDbbeUgqIiIijS54kQgICKC+vr5Fx1x11VV8/fXXdO3a9TylEhERESsueJFISEggLy+Pffv2ERIS4t51aM6sWbO48cYb6dy5M7/61a/w8/Nj+/bt7Nixg7lz516A1CIiIuLNBX+NxIMPPoi/vz89e/YkOjqagoKCHzwmIyOD1atX8/7779O/f38GDhzI888/T5cuXS5AYhERETkTwzRN82KHuBDKy8sJDw/n6DeXERbqM68xdcuIS7nYEUSklakza/mQVZSVlREWFnax48hPlO/9RhUREZFWQ0VCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxzFKR+O///m969+5NYGAgHTp0ID09nYqKCj777DOuueYaoqKiCA8PZ+jQoWzdutXjWMMw+NOf/sSNN95IUFAQPXr0IDc3l2+//ZZhw4YRHBzMoEGD2LNnj8dxq1at4qqrrsJut3PZZZfx+OOPU1dXZ/0nFxERkR+txUXi4MGDjBkzhjvuuINdu3bx4YcfcvPNN2OaJseOHWPcuHFs2LCBTZs2kZSUxIgRIzh27JjHOZ588knGjh3L559/Tvfu3fn1r3/N3XffzYwZM9iyZQumaXLfffe55z/55BPGjh3LpEmT2LlzJ3/6059YunQpTz311BlzVldXU15e7nERERGRc8swTdNsyQFbt26lb9++7Nu3jy5dujQ763K5iIiIYNmyZdx4440NV2gYPProozz55JMAbNq0ibS0NF599VXuuOMOAN544w3Gjx/PiRMnAEhPT2f48OHMmDHDfe7XX3+dadOm8f3333u97jlz5vD44483WT/6zWWEhfreMzoZcSkXO4KItDJ1Zi0fsoqysjLCwsIudhz5iWrxb9Tk5GSGDx9O7969ueWWW3jllVc4evQoAEVFRdx5550kJSURHh5OWFgYx48fp6CgwOMcffr0cf93bGwsAL179/ZYq6qqcu8ibN++nSeeeIKQkBD35c477+TgwYNUVlZ6zTljxgzKysrcl8LCwpb+qCIiIvID2rX0AH9/fz744AM2btzI+++/z8KFC3nkkUfIy8vj3nvvpaSkhAULFtClSxdsNhtpaWnU1NR4nKN9+/bu/zYM44xrLpcLgOPHj/P4449z8803N8ljt9u95rTZbNhstpb+eCIiItICLS4S0PCL/uc//zk///nPmTVrFl26dOHNN9/k008/5aWXXmLEiBEAFBYWcvjw4R8d8qqrruLrr7+ma9euP/pcIiIicu60uEjk5eWRk5PDtddeS0xMDHl5eRw6dIgePXqQlJTE3/72N/r160d5eTkPPfQQgYGBPzrkrFmzuPHGG+ncuTO/+tWv8PPzY/v27ezYsYO5c+f+6POLiIiINS1+jURYWBgff/wxI0aMoFu3bjz66KPMnz+f66+/nldffZWjR49y1VVX8Zvf/Ib777+fmJiYHx0yIyOD1atX8/7779O/f38GDhzI888//4Mv9hQREZHzq8Xv2mitysvLCQ8P17s2RKTN0Ls2xBf43m9UERERaTVUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxTEVCRERELFOREBEREctUJERERMQyFQkRERGxrN3FDnC+VFdXU11d7f5zeXn5RUwjIiLSNrXZHYmsrCzCw8Pdl/j4+IsdSUREpM1ps0VixowZlJWVuS+FhYUXO5KIiEib02af2rDZbNhstosdQ0REpE1rszsSIiIi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",
      "text/plain": [
       "<Figure size 400x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "'i m not to be a lot of the same .'"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 使用几个例子测试\n",
    "translator(u'Hoy es un buen día.')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2dcaf0b50008beb3",
   "metadata": {
    "ExecuteTime": {
     "start_time": "2025-03-11T05:26:24.027060Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 计算 BLEU 分数\n",
    "model = Sequence2Sequence(src_vocab_size=len(src_word2idx), trg_vocab_size=len(trg_word2idx), encoder_num_layers=4, decoder_num_layers=4)\n",
    "model.load_state_dict(torch.load(f\"./translate-seq2seq/best.ckpt\", map_location=\"cpu\"))\n",
    "\n",
    "class Translator:\n",
    "    def __init__(self, model, src_tokenizer, trg_tokenizer):\n",
    "        self.model = model\n",
    "        self.model.eval() # 切换到验证模式\n",
    "        self.src_tokenizer = src_tokenizer\n",
    "        self.trg_tokenizer = trg_tokenizer\n",
    "\n",
    "    def __call__(self, sentence):\n",
    "        sentence = preprocess_sentence(sentence) # 预处理句子，标点符号处理等\n",
    "        encoder_input, attn_mask = self.src_tokenizer.encode(\n",
    "            [sentence.split()],\n",
    "            padding_first=True,\n",
    "            add_bos=True,\n",
    "            add_eos=True,\n",
    "            return_mask=True,\n",
    "            ) # 对输入进行编码，并返回encode_piadding_mask\n",
    "        encoder_input = torch.Tensor(encoder_input).to(dtype=torch.int64) # 转换成tensor\n",
    "\n",
    "        preds, scores = model.infer(encoder_input=encoder_input, attn_mask=attn_mask) #预测\n",
    "\n",
    "        trg_sentence = self.trg_tokenizer.decode([preds], split=True, remove_eos=False)[0] #通过tokenizer转换成文字\n",
    "\n",
    "        return \" \".join(trg_sentence[:-1])\n",
    "\n",
    "from nltk.translate.bleu_score import sentence_bleu\n",
    "\n",
    "def evaluate_bleu_on_test_set(test_data, translator):\n",
    "    \"\"\"\n",
    "    在测试集上计算平均 BLEU 分数。\n",
    "    :param test_data: 测试集数据，格式为 [(src_sentence, [ref_translation1, ref_translation2, ...]), ...]\n",
    "    :param translator: 翻译器对象（Translator 类的实例）\n",
    "    :return: 平均 BLEU 分数\n",
    "    \"\"\"\n",
    "    total_bleu = 0.0\n",
    "    num_samples = len(test_data)\n",
    "    i=0\n",
    "    for src_sentence, ref_translations in test_data:\n",
    "        # 使用翻译器生成翻译结果\n",
    "        candidate_translation = translator(src_sentence)\n",
    "\n",
    "        # 计算 BLEU 分数\n",
    "        bleu_score = sentence_bleu([ref_translations.split()], candidate_translation.split(),weights=(1, 0, 0, 0))\n",
    "        total_bleu += bleu_score\n",
    "\n",
    "        # 打印当前句子的 BLEU 分数（可选）\n",
    "        # print(f\"Source: {src_sentence}\")\n",
    "        # print(f\"Reference: {ref_translations}\")\n",
    "        # print(f\"Candidate: {candidate_translation}\")\n",
    "        # print(f\"BLEU: {bleu_score:.4f}\")\n",
    "        # print(\"-\" * 50)\n",
    "        # i+=1\n",
    "        # if i>10:\n",
    "        #     break\n",
    "    # 计算平均 BLEU 分数\n",
    "    avg_bleu = total_bleu / num_samples\n",
    "    return avg_bleu\n",
    "translator = Translator(model.cpu(), src_tokenizer, trg_tokenizer)\n",
    "evaluate_bleu_on_test_set(test_ds, translator)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
